Add mechanisms for 3PID invite expiration and AS integration
- Integration with AS and a fallback user to decline expired invites (#120) - Rework of the AS feature to make it more independent/re-usable - Skeleton for admin interface via bot to manage invites (#138)
This commit is contained in:
@@ -26,6 +26,7 @@ import io.kamax.mxisd.http.undertow.handler.OptionsHandler;
|
|||||||
import io.kamax.mxisd.http.undertow.handler.SaneHandler;
|
import io.kamax.mxisd.http.undertow.handler.SaneHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler;
|
import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler;
|
import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler;
|
||||||
|
import io.kamax.mxisd.http.undertow.handler.as.v1.AsUserHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler;
|
import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
|
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
|
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
|
||||||
@@ -66,8 +67,11 @@ public class HttpMxisd {
|
|||||||
m.start();
|
m.start();
|
||||||
|
|
||||||
HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
|
HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
|
||||||
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
|
|
||||||
|
HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs()));
|
||||||
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
|
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
|
||||||
|
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
|
||||||
|
|
||||||
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager()));
|
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager()));
|
||||||
HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView()));
|
HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView()));
|
||||||
|
|
||||||
@@ -117,11 +121,12 @@ public class HttpMxisd {
|
|||||||
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvitationManager())))
|
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvitationManager())))
|
||||||
|
|
||||||
// Application Service endpoints
|
// Application Service endpoints
|
||||||
.get("/_matrix/app/v1/users/**", asNotFoundHandler)
|
.get(AsUserHandler.Path, asUserHandler)
|
||||||
.get("/users/**", asNotFoundHandler) // Legacy endpoint
|
|
||||||
.get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
|
.get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
|
||||||
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
|
|
||||||
.put(AsTransactionHandler.Path, asTxnHandler)
|
.put(AsTransactionHandler.Path, asTxnHandler)
|
||||||
|
|
||||||
|
.get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint
|
||||||
|
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
|
||||||
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
|
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
|
||||||
|
|
||||||
// Banned endpoints
|
// Banned endpoints
|
||||||
|
@@ -59,7 +59,9 @@ import java.util.ServiceLoader;
|
|||||||
|
|
||||||
public class Mxisd {
|
public class Mxisd {
|
||||||
|
|
||||||
|
public static final String Name = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationTitle(), "mxisd");
|
||||||
public static final String Version = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationVersion(), "UNKNOWN");
|
public static final String Version = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationVersion(), "UNKNOWN");
|
||||||
|
public static final String Agent = Name + "/" + Version;
|
||||||
|
|
||||||
private MxisdConfig cfg;
|
private MxisdConfig cfg;
|
||||||
|
|
||||||
@@ -89,7 +91,7 @@ public class Mxisd {
|
|||||||
|
|
||||||
private void build() {
|
private void build() {
|
||||||
httpClient = HttpClients.custom()
|
httpClient = HttpClients.custom()
|
||||||
.setUserAgent("mxisd/" + Version)
|
.setUserAgent(Agent)
|
||||||
.setMaxConnPerRoute(Integer.MAX_VALUE)
|
.setMaxConnPerRoute(Integer.MAX_VALUE)
|
||||||
.setMaxConnTotal(Integer.MAX_VALUE)
|
.setMaxConnTotal(Integer.MAX_VALUE)
|
||||||
.build();
|
.build();
|
||||||
|
@@ -23,11 +23,16 @@ package io.kamax.mxisd.as;
|
|||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import io.kamax.matrix.MatrixID;
|
import io.kamax.matrix.MatrixID;
|
||||||
import io.kamax.matrix._MatrixID;
|
import io.kamax.matrix._MatrixID;
|
||||||
|
import io.kamax.matrix.client.MatrixClientContext;
|
||||||
|
import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
|
||||||
import io.kamax.matrix.event.EventKey;
|
import io.kamax.matrix.event.EventKey;
|
||||||
import io.kamax.matrix.json.GsonUtil;
|
import io.kamax.matrix.json.GsonUtil;
|
||||||
|
import io.kamax.mxisd.as.processor.MembershipEventProcessor;
|
||||||
|
import io.kamax.mxisd.as.processor.MessageEventProcessor;
|
||||||
import io.kamax.mxisd.backend.sql.synapse.Synapse;
|
import io.kamax.mxisd.backend.sql.synapse.Synapse;
|
||||||
import io.kamax.mxisd.config.MatrixConfig;
|
import io.kamax.mxisd.config.AppServiceConfig;
|
||||||
import io.kamax.mxisd.config.MxisdConfig;
|
import io.kamax.mxisd.config.MxisdConfig;
|
||||||
|
import io.kamax.mxisd.exception.ConfigurationException;
|
||||||
import io.kamax.mxisd.exception.HttpMatrixException;
|
import io.kamax.mxisd.exception.HttpMatrixException;
|
||||||
import io.kamax.mxisd.exception.NotAllowedException;
|
import io.kamax.mxisd.exception.NotAllowedException;
|
||||||
import io.kamax.mxisd.notification.NotificationManager;
|
import io.kamax.mxisd.notification.NotificationManager;
|
||||||
@@ -36,6 +41,7 @@ import io.kamax.mxisd.storage.IStorage;
|
|||||||
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
|
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
|
||||||
import io.kamax.mxisd.util.GsonParser;
|
import io.kamax.mxisd.util.GsonParser;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -54,65 +60,145 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
|
|
||||||
public class AppSvcManager {
|
public class AppSvcManager {
|
||||||
|
|
||||||
private transient final Logger log = LoggerFactory.getLogger(AppSvcManager.class);
|
private static final Logger log = LoggerFactory.getLogger(AppSvcManager.class);
|
||||||
|
|
||||||
private final GsonParser parser;
|
private final AppServiceConfig cfg;
|
||||||
|
private final IStorage store;
|
||||||
|
private final GsonParser parser = new GsonParser();
|
||||||
|
|
||||||
private MatrixConfig cfg;
|
private MatrixApplicationServiceClient client;
|
||||||
private IStorage store;
|
|
||||||
private Map<String, EventTypeProcessor> processors = new HashMap<>();
|
private Map<String, EventTypeProcessor> processors = new HashMap<>();
|
||||||
|
private Map<String, CompletableFuture<String>> transactionsInProgress = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private Map<String, CompletableFuture<String>> transactionsInProgress;
|
public AppSvcManager(MxisdConfig mxisdCfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) {
|
||||||
|
this.cfg = mxisdCfg.getAppsvc();
|
||||||
public AppSvcManager(MxisdConfig cfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) {
|
|
||||||
this.cfg = cfg.getMatrix();
|
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
|
||||||
parser = new GsonParser();
|
/*
|
||||||
transactionsInProgress = new ConcurrentHashMap<>();
|
We process the configuration to make sure all is fine and setting default values if needed
|
||||||
|
*/
|
||||||
|
|
||||||
processors.put("m.room.member", new MembershipProcessor(cfg.getMatrix(), profiler, notif, synapse));
|
// By default, the feature is enabled
|
||||||
|
cfg.setEnabled(ObjectUtils.defaultIfNull(cfg.isEnabled(), false));
|
||||||
|
|
||||||
processConfig();
|
if (!cfg.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(cfg.getEndpoint().getToAS().getUrl())) {
|
||||||
|
throw new ConfigurationException("App Service: Endpoint: To AS: URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(cfg.getEndpoint().getToAS().getToken())) {
|
||||||
|
throw new ConfigurationException("App Service: Endpoint: To AS: Token", "Must be set, even if to an empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(cfg.getEndpoint().getToHS().getUrl())) {
|
||||||
|
throw new ConfigurationException("App Service: Endpoint: To HS: URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(cfg.getEndpoint().getToHS().getToken())) {
|
||||||
|
throw new ConfigurationException("App Service: Endpoint: To HS: Token", "Must be set, even if to an empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set a default status for each feature individually
|
||||||
|
cfg.getFeature().getAdmin().setEnabled(ObjectUtils.defaultIfNull(cfg.getFeature().getAdmin().getEnabled(), cfg.isEnabled()));
|
||||||
|
cfg.getFeature().setCleanExpiredInvite(ObjectUtils.defaultIfNull(cfg.getFeature().getCleanExpiredInvite(), cfg.isEnabled()));
|
||||||
|
cfg.getFeature().setInviteById(ObjectUtils.defaultIfNull(cfg.getFeature().getInviteById(), false));
|
||||||
|
|
||||||
|
if (cfg.getFeature().getAdmin().getEnabled()) {
|
||||||
|
if (StringUtils.isBlank(cfg.getUser().getMain())) {
|
||||||
|
throw new ConfigurationException("App Service admin feature is enabled, but no main user configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) {
|
||||||
|
throw new ConfigurationException("App Service: Users: Main ID: Is not a localpart");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.getFeature().getCleanExpiredInvite()) {
|
||||||
|
if (StringUtils.isBlank(cfg.getUser().getInviteExpired())) {
|
||||||
|
throw new ConfigurationException("App Service user for Expired Invite is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) {
|
||||||
|
throw new ConfigurationException("App Service: Users: Expired Invite ID: Is not a localpart");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixClientContext mxContext = new MatrixClientContext();
|
||||||
|
mxContext.setDomain(mxisdCfg.getMatrix().getDomain());
|
||||||
|
mxContext.setToken(cfg.getEndpoint().getToHS().getToken());
|
||||||
|
mxContext.setHsBaseUrl(cfg.getEndpoint().getToHS().getUrl());
|
||||||
|
client = new MatrixApplicationServiceClient(mxContext);
|
||||||
|
|
||||||
|
processors.put("m.room.member", new MembershipEventProcessor(client, mxisdCfg, profiler, notif, synapse));
|
||||||
|
processors.put("m.room.message", new MessageEventProcessor(client));
|
||||||
|
|
||||||
|
processSynapseConfig(mxisdCfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processConfig() {
|
private void processSynapseConfig(MxisdConfig cfg) {
|
||||||
String synapseRegFile = cfg.getListener().getSynapse().getRegistrationFile();
|
String synapseRegFile = cfg.getAppsvc().getRegistration().getSynapse().getFile();
|
||||||
if (StringUtils.isNotBlank(synapseRegFile)) {
|
|
||||||
SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getListener());
|
|
||||||
|
|
||||||
Representer rep = new Representer();
|
if (StringUtils.isBlank(synapseRegFile)) {
|
||||||
rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD);
|
log.info("No synapse registration file path given - skipping generation...");
|
||||||
Yaml yaml = new Yaml(rep);
|
return;
|
||||||
String synCfgRaw = yaml.dump(syncCfg);
|
}
|
||||||
|
|
||||||
try {
|
SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getAppsvc(), cfg.getMatrix().getDomain());
|
||||||
IOUtils.write(synCfgRaw, new FileOutputStream(synapseRegFile), StandardCharsets.UTF_8);
|
|
||||||
} catch (IOException e) {
|
Representer rep = new Representer();
|
||||||
throw new RuntimeException("Unable to write synapse appservice registration file", e);
|
rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD);
|
||||||
}
|
Yaml yaml = new Yaml(rep);
|
||||||
|
|
||||||
|
// SnakeYAML set the type of object on the first line, which can fail to be parsed on synapse
|
||||||
|
// We therefore need to split the resulting string, remove the first line, and then write it
|
||||||
|
List<String> lines = new ArrayList<>(Arrays.asList(yaml.dump(syncCfg).split("\\R+")));
|
||||||
|
if (StringUtils.equals(lines.get(0), "!!" + SynapseRegistrationYaml.class.getCanonicalName())) {
|
||||||
|
lines.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileOutputStream os = new FileOutputStream(synapseRegFile)) {
|
||||||
|
IOUtils.writeLines(lines, System.lineSeparator(), os, StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unable to write synapse appservice registration file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureEnabled() {
|
||||||
|
if (!cfg.isEnabled()) {
|
||||||
|
throw new HttpMatrixException(503, "M_NOT_AVAILABLE", "This feature is disabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AppSvcManager withToken(String token) {
|
public AppSvcManager withToken(String token) {
|
||||||
|
ensureEnabled();
|
||||||
|
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token");
|
throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtils.equals(cfg.getListener().getToken().getHs(), token)) {
|
if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) {
|
||||||
throw new NotAllowedException("Invalid HS token");
|
throw new NotAllowedException("Invalid HS token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void processUser(String userId) {
|
||||||
|
client.createUser(MatrixID.asAcceptable(userId).getLocalPart());
|
||||||
|
}
|
||||||
|
|
||||||
public CompletableFuture<String> processTransaction(String txnId, InputStream is) {
|
public CompletableFuture<String> processTransaction(String txnId, InputStream is) {
|
||||||
|
ensureEnabled();
|
||||||
|
|
||||||
if (StringUtils.isEmpty(txnId)) {
|
if (StringUtils.isEmpty(txnId)) {
|
||||||
throw new IllegalArgumentException("Transaction ID cannot be empty");
|
throw new IllegalArgumentException("Transaction ID cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getListener().getLocalpart(), txnId);
|
Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getUser().getMain(), txnId);
|
||||||
if (dao.isPresent()) {
|
if (dao.isPresent()) {
|
||||||
log.info("AS Transaction {} already processed - returning computed result", txnId);
|
log.info("AS Transaction {} already processed - returning computed result", txnId);
|
||||||
return CompletableFuture.completedFuture(dao.get().getResult());
|
return CompletableFuture.completedFuture(dao.get().getResult());
|
||||||
@@ -143,7 +229,7 @@ public class AppSvcManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("Saving transaction details to store");
|
log.info("Saving transaction details to store");
|
||||||
store.insertTransactionResult(cfg.getListener().getLocalpart(), txnId, end, result);
|
store.insertTransactionResult(cfg.getUser().getMain(), txnId, end, result);
|
||||||
} finally {
|
} finally {
|
||||||
log.debug("Removing CompletedFuture from transaction map");
|
log.debug("Removing CompletedFuture from transaction map");
|
||||||
transactionsInProgress.remove(txnId);
|
transactionsInProgress.remove(txnId);
|
||||||
|
@@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2019 Kamax Sarl
|
|
||||||
*
|
|
||||||
* https://www.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.as;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import io.kamax.matrix.MatrixID;
|
|
||||||
import io.kamax.matrix.ThreePidMedium;
|
|
||||||
import io.kamax.matrix._MatrixID;
|
|
||||||
import io.kamax.matrix._ThreePid;
|
|
||||||
import io.kamax.matrix.event.EventKey;
|
|
||||||
import io.kamax.mxisd.backend.sql.synapse.Synapse;
|
|
||||||
import io.kamax.mxisd.config.MatrixConfig;
|
|
||||||
import io.kamax.mxisd.notification.NotificationManager;
|
|
||||||
import io.kamax.mxisd.profile.ProfileManager;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class MembershipProcessor implements EventTypeProcessor {
|
|
||||||
|
|
||||||
private final static Logger log = LoggerFactory.getLogger(MembershipProcessor.class);
|
|
||||||
|
|
||||||
private final MatrixConfig cfg;
|
|
||||||
private ProfileManager profiler;
|
|
||||||
private NotificationManager notif;
|
|
||||||
private Synapse synapse;
|
|
||||||
|
|
||||||
public MembershipProcessor(MatrixConfig cfg, ProfileManager profiler, NotificationManager notif, Synapse synapse) {
|
|
||||||
this.cfg = cfg;
|
|
||||||
this.profiler = profiler;
|
|
||||||
this.notif = notif;
|
|
||||||
this.synapse = synapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void process(JsonObject ev, _MatrixID sender, String roomId) {
|
|
||||||
JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> {
|
|
||||||
log.debug("No content found, falling back to full object");
|
|
||||||
return ev;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) {
|
|
||||||
log.debug("This is not an invite event, skipping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String inviteeId = EventKey.StateKey.getStringOrNull(ev);
|
|
||||||
if (StringUtils.isBlank(inviteeId)) {
|
|
||||||
log.warn("Invalid event: No invitee ID, skipping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_MatrixID invitee = MatrixID.asAcceptable(inviteeId);
|
|
||||||
if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) {
|
|
||||||
log.debug("Ignoring invite for {}: not a local user");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Got invite from {} to {}", sender.getId(), inviteeId);
|
|
||||||
|
|
||||||
boolean wasSent = false;
|
|
||||||
List<_ThreePid> tpids = profiler.getThreepids(invitee).stream()
|
|
||||||
.filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId);
|
|
||||||
|
|
||||||
for (_ThreePid tpid : tpids) {
|
|
||||||
log.info("Found Email to notify about room invitation: {}", tpid.getAddress());
|
|
||||||
Map<String, String> properties = new HashMap<>();
|
|
||||||
profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name));
|
|
||||||
try {
|
|
||||||
synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name));
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
log.warn("Could not fetch room name", e);
|
|
||||||
log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?");
|
|
||||||
}
|
|
||||||
|
|
||||||
IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties);
|
|
||||||
notif.sendForInvite(inv);
|
|
||||||
log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress());
|
|
||||||
wasSent = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Was notification sent? {}", wasSent);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.as;
|
package io.kamax.mxisd.as;
|
||||||
|
|
||||||
import io.kamax.mxisd.config.ListenerConfig;
|
import io.kamax.mxisd.config.AppServiceConfig;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -29,20 +29,28 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public class SynapseRegistrationYaml {
|
public class SynapseRegistrationYaml {
|
||||||
|
|
||||||
public static SynapseRegistrationYaml parse(ListenerConfig cfg) {
|
public static SynapseRegistrationYaml parse(AppServiceConfig cfg, String domain) {
|
||||||
SynapseRegistrationYaml yaml = new SynapseRegistrationYaml();
|
SynapseRegistrationYaml yaml = new SynapseRegistrationYaml();
|
||||||
|
|
||||||
yaml.setId("appservice-mxisd");
|
yaml.setId(cfg.getRegistration().getSynapse().getId());
|
||||||
yaml.setUrl(cfg.getUrl());
|
yaml.setUrl(cfg.getEndpoint().getToAS().getUrl());
|
||||||
yaml.setAsToken(cfg.getToken().getAs());
|
yaml.setAsToken(cfg.getEndpoint().getToHS().getToken());
|
||||||
yaml.setHsToken(cfg.getToken().getHs());
|
yaml.setHsToken(cfg.getEndpoint().getToAS().getToken());
|
||||||
yaml.setSenderLocalpart(cfg.getLocalpart());
|
yaml.setSenderLocalpart(cfg.getUser().getMain());
|
||||||
cfg.getUsers().forEach(template -> {
|
|
||||||
|
if (cfg.getFeature().getCleanExpiredInvite()) {
|
||||||
Namespace ns = new Namespace();
|
Namespace ns = new Namespace();
|
||||||
ns.setRegex(template.getTemplate());
|
|
||||||
ns.setExclusive(true);
|
ns.setExclusive(true);
|
||||||
|
ns.setRegex("@" + cfg.getUser().getInviteExpired() + ":" + domain);
|
||||||
yaml.getNamespaces().getUsers().add(ns);
|
yaml.getNamespaces().getUsers().add(ns);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (cfg.getFeature().getInviteById()) {
|
||||||
|
Namespace ns = new Namespace();
|
||||||
|
ns.setExclusive(false);
|
||||||
|
ns.setRegex("@*:" + domain);
|
||||||
|
yaml.getNamespaces().getUsers().add(ns);
|
||||||
|
}
|
||||||
|
|
||||||
return yaml;
|
return yaml;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2019 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.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.as.processor;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.kamax.matrix.MatrixID;
|
||||||
|
import io.kamax.matrix.ThreePidMedium;
|
||||||
|
import io.kamax.matrix._MatrixID;
|
||||||
|
import io.kamax.matrix._ThreePid;
|
||||||
|
import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
|
||||||
|
import io.kamax.matrix.event.EventKey;
|
||||||
|
import io.kamax.matrix.hs._MatrixRoom;
|
||||||
|
import io.kamax.mxisd.as.EventTypeProcessor;
|
||||||
|
import io.kamax.mxisd.as.IMatrixIdInvite;
|
||||||
|
import io.kamax.mxisd.as.MatrixIdInvite;
|
||||||
|
import io.kamax.mxisd.backend.sql.synapse.Synapse;
|
||||||
|
import io.kamax.mxisd.config.MxisdConfig;
|
||||||
|
import io.kamax.mxisd.notification.NotificationManager;
|
||||||
|
import io.kamax.mxisd.profile.ProfileManager;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class MembershipEventProcessor implements EventTypeProcessor {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(MembershipEventProcessor.class);
|
||||||
|
|
||||||
|
private MatrixApplicationServiceClient client;
|
||||||
|
|
||||||
|
private final MxisdConfig cfg;
|
||||||
|
private ProfileManager profiler;
|
||||||
|
private NotificationManager notif;
|
||||||
|
private Synapse synapse;
|
||||||
|
|
||||||
|
public MembershipEventProcessor(
|
||||||
|
MatrixApplicationServiceClient client,
|
||||||
|
MxisdConfig cfg,
|
||||||
|
ProfileManager profiler,
|
||||||
|
NotificationManager notif,
|
||||||
|
Synapse synapse
|
||||||
|
) {
|
||||||
|
this.client = client;
|
||||||
|
this.cfg = cfg;
|
||||||
|
this.profiler = profiler;
|
||||||
|
this.notif = notif;
|
||||||
|
this.synapse = synapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(JsonObject ev, _MatrixID sender, String roomId) {
|
||||||
|
JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> {
|
||||||
|
log.debug("No content found, falling back to full object");
|
||||||
|
return ev;
|
||||||
|
});
|
||||||
|
|
||||||
|
String targetId = EventKey.StateKey.getStringOrNull(ev);
|
||||||
|
if (StringUtils.isBlank(targetId)) {
|
||||||
|
log.warn("Invalid event: No invitee ID, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_MatrixID target = MatrixID.asAcceptable(targetId);
|
||||||
|
if (!StringUtils.equals(target.getDomain(), cfg.getMatrix().getDomain())) {
|
||||||
|
log.debug("Ignoring invite for {}: not a local user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Got membership event from {} to {} for room {}", sender.getId(), targetId, roomId);
|
||||||
|
|
||||||
|
boolean isForMainUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getMain());
|
||||||
|
boolean isForExpInvUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getInviteExpired());
|
||||||
|
boolean isUs = isForMainUser || isForExpInvUser;
|
||||||
|
|
||||||
|
if (StringUtils.equals("join", EventKey.Membership.getStringOrNull(content))) {
|
||||||
|
if (!isForMainUser) {
|
||||||
|
log.warn("We joined the room {} for another identity as the main user, which is not supported. Leaving...", roomId);
|
||||||
|
|
||||||
|
client.getUser(target.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> {
|
||||||
|
log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) {
|
||||||
|
if (isForMainUser) {
|
||||||
|
processForMainUser(roomId, sender);
|
||||||
|
} else if (isForExpInvUser) {
|
||||||
|
processForExpiredInviteUser(roomId, target);
|
||||||
|
} else {
|
||||||
|
processForUserIdInvite(roomId, sender, target);
|
||||||
|
}
|
||||||
|
} else if (StringUtils.equals("leave", EventKey.Membership.getStringOrNull(content))) {
|
||||||
|
_MatrixRoom room = client.getRoom(roomId);
|
||||||
|
if (!isUs && room.getJoinedUsers().size() == 1) {
|
||||||
|
// TODO we need to find out if this is only us remaining and leave the room if so, using the right client for it
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("This is not an supported type of membership event, skipping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processForMainUser(String roomId, _MatrixID sender) {
|
||||||
|
List<String> roles = profiler.getRoles(sender);
|
||||||
|
if (Collections.disjoint(roles, cfg.getAppsvc().getFeature().getAdmin().getAllowedRoles())) {
|
||||||
|
log.info("Sender does not have any of the required roles, denying");
|
||||||
|
client.getRoom(roomId).tryLeave().ifPresent(err -> {
|
||||||
|
log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client.getRoom(roomId).tryJoin().ifPresent(err -> {
|
||||||
|
log.warn("Could not join room {}: {} - {}", roomId, err.getErrcode(), err.getError());
|
||||||
|
client.getRoom(roomId).tryLeave().ifPresent(err1 -> {
|
||||||
|
log.warn("Could not decline invite to room {} after failed join: {} - {}", roomId, err1.getErrcode(), err1.getError());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processForExpiredInviteUser(String roomId, _MatrixID invitee) {
|
||||||
|
client.getUser(invitee.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> {
|
||||||
|
log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processForUserIdInvite(String roomId, _MatrixID sender, _MatrixID invitee) {
|
||||||
|
String inviteeId = invitee.getId();
|
||||||
|
|
||||||
|
boolean wasSent = false;
|
||||||
|
List<_ThreePid> tpids = profiler.getThreepids(invitee).stream()
|
||||||
|
.filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId);
|
||||||
|
|
||||||
|
for (_ThreePid tpid : tpids) {
|
||||||
|
log.info("Found Email to notify about room invitation: {}", tpid.getAddress());
|
||||||
|
Map<String, String> properties = new HashMap<>();
|
||||||
|
profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name));
|
||||||
|
try {
|
||||||
|
synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warn("Could not fetch room name", e);
|
||||||
|
log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?");
|
||||||
|
}
|
||||||
|
|
||||||
|
IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties);
|
||||||
|
notif.sendForInvite(inv);
|
||||||
|
log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress());
|
||||||
|
wasSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Was notification sent? {}", wasSent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2019 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.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.as.processor;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.kamax.matrix._MatrixID;
|
||||||
|
import io.kamax.matrix._MatrixUserProfile;
|
||||||
|
import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
|
||||||
|
import io.kamax.matrix.hs._MatrixRoom;
|
||||||
|
import io.kamax.matrix.json.event.MatrixJsonRoomMessageEvent;
|
||||||
|
import io.kamax.mxisd.as.EventTypeProcessor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class MessageEventProcessor implements EventTypeProcessor {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MessageEventProcessor.class);
|
||||||
|
|
||||||
|
private final MatrixApplicationServiceClient client;
|
||||||
|
|
||||||
|
public MessageEventProcessor(MatrixApplicationServiceClient client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(JsonObject ev, _MatrixID sender, String roomId) {
|
||||||
|
_MatrixRoom room = client.getRoom(roomId);
|
||||||
|
List<_MatrixID> joinedUsers = room.getJoinedUsers().stream().map(_MatrixUserProfile::getId).collect(Collectors.toList());
|
||||||
|
boolean joinedWithMainUser = joinedUsers.contains(client.getWhoAmI());
|
||||||
|
boolean isAdminPrivate = joinedWithMainUser && joinedUsers.size() == 2;
|
||||||
|
|
||||||
|
MatrixJsonRoomMessageEvent msgEv = new MatrixJsonRoomMessageEvent(ev);
|
||||||
|
if (StringUtils.equals("m.notice", msgEv.getBodyType())) {
|
||||||
|
log.info("Ignoring automated message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtils.equals("m.text", msgEv.getBodyType())) {
|
||||||
|
log.info("Unsupported message event type: {}", msgEv.getBodyType());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String command = msgEv.getBody();
|
||||||
|
if (!isAdminPrivate) {
|
||||||
|
if (StringUtils.equals(command, "!mxisd")) {
|
||||||
|
// TODO show help
|
||||||
|
}
|
||||||
|
if (!StringUtils.startsWith(command, "!mxisd ")) {
|
||||||
|
// Not for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
command = command.substring("!mxisd ".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.equals("ping", command)) {
|
||||||
|
room.sendText("Pong!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
287
src/main/java/io/kamax/mxisd/config/AppServiceConfig.java
Normal file
287
src/main/java/io/kamax/mxisd/config/AppServiceConfig.java
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2018 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.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.config;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.Mxisd;
|
||||||
|
import io.kamax.mxisd.exception.ConfigurationException;
|
||||||
|
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class AppServiceConfig {
|
||||||
|
|
||||||
|
public static class Users {
|
||||||
|
|
||||||
|
private String main = "mxisd";
|
||||||
|
private String inviteExpired = "_mxisd_invite-expired";
|
||||||
|
|
||||||
|
public String getMain() {
|
||||||
|
return main;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMain(String main) {
|
||||||
|
this.main = main;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInviteExpired() {
|
||||||
|
return inviteExpired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInviteExpired(String inviteExpired) {
|
||||||
|
this.inviteExpired = inviteExpired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Endpoint {
|
||||||
|
|
||||||
|
private String url;
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
private transient URL cUrl;
|
||||||
|
|
||||||
|
public URL getUrl() {
|
||||||
|
return cUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
if (Objects.isNull(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cUrl = new URL(url);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new ConfigurationException("AppService endpoint(s) URL definition");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Endpoints {
|
||||||
|
|
||||||
|
private Endpoint toAS = new Endpoint();
|
||||||
|
private Endpoint toHS = new Endpoint();
|
||||||
|
|
||||||
|
public Endpoint getToAS() {
|
||||||
|
return toAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToAS(Endpoint toAS) {
|
||||||
|
this.toAS = toAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Endpoint getToHS() {
|
||||||
|
return toHS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToHS(Endpoint toHS) {
|
||||||
|
this.toHS = toHS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
toAS.build();
|
||||||
|
toHS.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Synapse {
|
||||||
|
|
||||||
|
private String id = "appservice-" + Mxisd.Name;
|
||||||
|
private String file;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFile() {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFile(String file) {
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Registration {
|
||||||
|
|
||||||
|
private Synapse synapse = new Synapse();
|
||||||
|
|
||||||
|
public Synapse getSynapse() {
|
||||||
|
return synapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSynapse(Synapse synapse) {
|
||||||
|
this.synapse = synapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
synapse.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AdminFeature {
|
||||||
|
|
||||||
|
private Boolean enabled;
|
||||||
|
private List<String> allowedRoles = new ArrayList<>();
|
||||||
|
|
||||||
|
public Boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedRoles() {
|
||||||
|
return allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedRoles(List<String> allowedRoles) {
|
||||||
|
this.allowedRoles = allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Features {
|
||||||
|
|
||||||
|
private AdminFeature admin = new AdminFeature();
|
||||||
|
private Boolean inviteById;
|
||||||
|
private Boolean cleanExpiredInvite;
|
||||||
|
|
||||||
|
public AdminFeature getAdmin() {
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdmin(AdminFeature admin) {
|
||||||
|
this.admin = admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getInviteById() {
|
||||||
|
return inviteById;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInviteById(Boolean inviteById) {
|
||||||
|
this.inviteById = inviteById;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getCleanExpiredInvite() {
|
||||||
|
return cleanExpiredInvite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCleanExpiredInvite(Boolean cleanExpiredInvite) {
|
||||||
|
this.cleanExpiredInvite = cleanExpiredInvite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
admin.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean enabled;
|
||||||
|
private Features feature = new Features();
|
||||||
|
private Endpoints endpoint = new Endpoints();
|
||||||
|
private Registration registration = new Registration();
|
||||||
|
private Users user = new Users();
|
||||||
|
|
||||||
|
public Boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Features getFeature() {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeature(Features feature) {
|
||||||
|
this.feature = feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Endpoints getEndpoint() {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndpoint(Endpoints endpoint) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Registration getRegistration() {
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegistration(Registration registration) {
|
||||||
|
this.registration = registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Users getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(Users user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
endpoint.build();
|
||||||
|
feature.build();
|
||||||
|
registration.build();
|
||||||
|
user.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,178 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2018 Kamax Sarl
|
|
||||||
*
|
|
||||||
* https://www.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.config;
|
|
||||||
|
|
||||||
import io.kamax.mxisd.exception.ConfigurationException;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ListenerConfig {
|
|
||||||
|
|
||||||
public static class Synpase {
|
|
||||||
|
|
||||||
private String registrationFile;
|
|
||||||
|
|
||||||
public String getRegistrationFile() {
|
|
||||||
return registrationFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRegistrationFile(String registrationFile) {
|
|
||||||
this.registrationFile = registrationFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UserTemplate {
|
|
||||||
|
|
||||||
private String type = "regex";
|
|
||||||
private String template;
|
|
||||||
|
|
||||||
public String getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(String type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTemplate() {
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTemplate(String template) {
|
|
||||||
this.template = template;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Token {
|
|
||||||
|
|
||||||
private String as;
|
|
||||||
private String hs;
|
|
||||||
|
|
||||||
public String getAs() {
|
|
||||||
return as;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAs(String as) {
|
|
||||||
this.as = as;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHs() {
|
|
||||||
return hs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHs(String hs) {
|
|
||||||
this.hs = hs;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private String id = "appservice-mxisd";
|
|
||||||
private String url;
|
|
||||||
private String localpart = "mxisd";
|
|
||||||
private Token token = new Token();
|
|
||||||
private List<UserTemplate> users = new ArrayList<>();
|
|
||||||
private Synpase synapse = new Synpase();
|
|
||||||
|
|
||||||
private transient URL csUrl;
|
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public URL getUrl() {
|
|
||||||
return csUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocalpart() {
|
|
||||||
return localpart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocalpart(String localpart) {
|
|
||||||
this.localpart = localpart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Token getToken() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToken(Token token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UserTemplate> getUsers() {
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUsers(List<UserTemplate> users) {
|
|
||||||
this.users = users;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Synpase getSynapse() {
|
|
||||||
return synapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSynapse(Synpase synapse) {
|
|
||||||
this.synapse = synapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void build() {
|
|
||||||
try {
|
|
||||||
if (StringUtils.isBlank(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
csUrl = new URL(url);
|
|
||||||
|
|
||||||
if (org.apache.commons.lang3.StringUtils.isBlank(getId())) {
|
|
||||||
throw new IllegalArgumentException("Matrix Listener ID is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(getLocalpart())) {
|
|
||||||
throw new IllegalArgumentException("localpart for matrix listener is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(getToken().getAs())) {
|
|
||||||
throw new IllegalArgumentException("AS token is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(getToken().getHs())) {
|
|
||||||
throw new IllegalArgumentException("HS token is not set");
|
|
||||||
}
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
throw new ConfigurationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -63,7 +63,6 @@ public class MatrixConfig {
|
|||||||
|
|
||||||
private String domain;
|
private String domain;
|
||||||
private Identity identity = new Identity();
|
private Identity identity = new Identity();
|
||||||
private ListenerConfig listener = new ListenerConfig();
|
|
||||||
|
|
||||||
public String getDomain() {
|
public String getDomain() {
|
||||||
return domain;
|
return domain;
|
||||||
@@ -81,14 +80,6 @@ public class MatrixConfig {
|
|||||||
this.identity = identity;
|
this.identity = identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenerConfig getListener() {
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(ListenerConfig listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void build() {
|
public void build() {
|
||||||
log.info("--- Matrix config ---");
|
log.info("--- Matrix config ---");
|
||||||
|
|
||||||
@@ -99,8 +90,6 @@ public class MatrixConfig {
|
|||||||
log.info("Domain: {}", getDomain());
|
log.info("Domain: {}", getDomain());
|
||||||
log.info("Identity:");
|
log.info("Identity:");
|
||||||
log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers()));
|
log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers()));
|
||||||
|
|
||||||
listener.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -83,6 +83,7 @@ public class MxisdConfig {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AppServiceConfig appsvc = new AppServiceConfig();
|
||||||
private AuthenticationConfig auth = new AuthenticationConfig();
|
private AuthenticationConfig auth = new AuthenticationConfig();
|
||||||
private DirectoryConfig directory = new DirectoryConfig();
|
private DirectoryConfig directory = new DirectoryConfig();
|
||||||
private Dns dns = new Dns();
|
private Dns dns = new Dns();
|
||||||
@@ -108,6 +109,14 @@ public class MxisdConfig {
|
|||||||
private ViewConfig view = new ViewConfig();
|
private ViewConfig view = new ViewConfig();
|
||||||
private WordpressConfig wordpress = new WordpressConfig();
|
private WordpressConfig wordpress = new WordpressConfig();
|
||||||
|
|
||||||
|
public AppServiceConfig getAppsvc() {
|
||||||
|
return appsvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppsvc(AppServiceConfig appsvc) {
|
||||||
|
this.appsvc = appsvc;
|
||||||
|
}
|
||||||
|
|
||||||
public AuthenticationConfig getAuth() {
|
public AuthenticationConfig getAuth() {
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
@@ -306,6 +315,7 @@ public class MxisdConfig {
|
|||||||
log.debug("server.name is empty, using matrix.domain");
|
log.debug("server.name is empty, using matrix.domain");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAppsvc().build();
|
||||||
getAuth().build();
|
getAuth().build();
|
||||||
getDirectory().build();
|
getDirectory().build();
|
||||||
getExec().build();
|
getExec().build();
|
||||||
|
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2019 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.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.http.undertow.handler.as.v1;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.as.AppSvcManager;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
public class AsUserHandler extends ApplicationServiceHandler {
|
||||||
|
|
||||||
|
public static final String ID = "userId";
|
||||||
|
public static final String Path = "/_matrix/app/v1/users/{" + ID + "}";
|
||||||
|
|
||||||
|
private final AppSvcManager app;
|
||||||
|
|
||||||
|
public AsUserHandler(AppSvcManager app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange exchange) {
|
||||||
|
String userId = exchange.getQueryParameters().getOrDefault(ID, new LinkedList<>()).peekFirst();
|
||||||
|
app.withToken(getToken(exchange)).processUser(userId);
|
||||||
|
respondJson(exchange, "{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -34,7 +34,7 @@ public class VersionHandler extends BasicHttpHandler {
|
|||||||
|
|
||||||
public VersionHandler() {
|
public VersionHandler() {
|
||||||
JsonObject server = new JsonObject();
|
JsonObject server = new JsonObject();
|
||||||
server.addProperty("name", "mxisd");
|
server.addProperty("name", Mxisd.Name);
|
||||||
server.addProperty("version", Mxisd.Version);
|
server.addProperty("version", Mxisd.Version);
|
||||||
|
|
||||||
body = GsonUtil.getPrettyForLog(GsonUtil.makeObj("server", server));
|
body = GsonUtil.getPrettyForLog(GsonUtil.makeObj("server", server));
|
||||||
|
@@ -186,7 +186,12 @@ public class InvitationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) {
|
if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) {
|
||||||
throw new ConfigurationException("Invitation expiration resolution target cannot be empty/blank");
|
String localpart = cfg.getAppsvc().getUser().getInviteExpired();
|
||||||
|
if (StringUtils.isBlank(localpart)) {
|
||||||
|
throw new ConfigurationException("Could not compute the Invitation expiration resolution target from App service user: not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.getInvite().getExpiration().setResolveTo(MatrixID.asAcceptable(localpart, cfg.getMatrix().getDomain()).getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -395,8 +400,8 @@ public class InvitationManager {
|
|||||||
Instant ts = Instant.ofEpochMilli(Long.parseLong(tsRaw));
|
Instant ts = Instant.ofEpochMilli(Long.parseLong(tsRaw));
|
||||||
Instant targetTs = ts.plusSeconds(cfg.getExpiration().getAfter() * 60);
|
Instant targetTs = ts.plusSeconds(cfg.getExpiration().getAfter() * 60);
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
log.debug("Invite {} - Created at {} - Expire at {} - Current time is {}", reply.getId(), ts, targetTs, now);
|
log.debug("Invite {} - Created at {} - Expires at {} - Current time is {}", reply.getId(), ts, targetTs, now);
|
||||||
if (targetTs.isBefore(Instant.now())) {
|
if (targetTs.isAfter(now)) {
|
||||||
log.debug("Invite {} has not expired yet, skipping", reply.getId());
|
log.debug("Invite {} has not expired yet, skipping", reply.getId());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -494,6 +499,7 @@ public class InvitationManager {
|
|||||||
|
|
||||||
Instant resolvedAt = Instant.now();
|
Instant resolvedAt = Instant.now();
|
||||||
boolean couldPublish = false;
|
boolean couldPublish = false;
|
||||||
|
boolean shouldArchive = true;
|
||||||
try {
|
try {
|
||||||
log.info("Posting onBind event to {}", req.getURI());
|
log.info("Posting onBind event to {}", req.getURI());
|
||||||
CloseableHttpResponse response = client.execute(req);
|
CloseableHttpResponse response = client.execute(req);
|
||||||
@@ -501,7 +507,12 @@ public class InvitationManager {
|
|||||||
log.info("Answer code: {}", statusCode);
|
log.info("Answer code: {}", statusCode);
|
||||||
if (statusCode >= 300 && statusCode != 403) {
|
if (statusCode >= 300 && statusCode != 403) {
|
||||||
log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
|
log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
|
||||||
log.warn("HS returned an error. Invite can be found in historical storage for manual re-processing");
|
log.warn("HS returned an error.");
|
||||||
|
|
||||||
|
shouldArchive = statusCode != 502;
|
||||||
|
if (shouldArchive) {
|
||||||
|
log.info("Invite can be found in historical storage for manual re-processing");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
couldPublish = true;
|
couldPublish = true;
|
||||||
if (statusCode == 403) {
|
if (statusCode == 403) {
|
||||||
@@ -512,10 +523,12 @@ public class InvitationManager {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
||||||
} finally {
|
} finally {
|
||||||
synchronized (this) {
|
if (shouldArchive) {
|
||||||
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
|
synchronized (this) {
|
||||||
removeInvite(reply);
|
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
|
||||||
log.info("Moved invite {} to historical table", reply.getId());
|
removeInvite(reply);
|
||||||
|
log.info("Moved invite {} to historical table", reply.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
|
@@ -22,6 +22,7 @@ package io.kamax.mxisd.threepid.connector.email;
|
|||||||
|
|
||||||
import com.sun.mail.smtp.SMTPTransport;
|
import com.sun.mail.smtp.SMTPTransport;
|
||||||
import io.kamax.matrix.ThreePidMedium;
|
import io.kamax.matrix.ThreePidMedium;
|
||||||
|
import io.kamax.mxisd.Mxisd;
|
||||||
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
|
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
|
||||||
import io.kamax.mxisd.exception.FeatureNotAvailable;
|
import io.kamax.mxisd.exception.FeatureNotAvailable;
|
||||||
import io.kamax.mxisd.exception.InternalServerError;
|
import io.kamax.mxisd.exception.InternalServerError;
|
||||||
@@ -92,7 +93,7 @@ public class EmailSmtpConnector implements EmailConnector {
|
|||||||
try {
|
try {
|
||||||
InternetAddress sender = new InternetAddress(senderAddress, senderName);
|
InternetAddress sender = new InternetAddress(senderAddress, senderName);
|
||||||
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
|
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
|
||||||
msg.setHeader("X-Mailer", "mxisd"); // FIXME set version
|
msg.setHeader("X-Mailer", Mxisd.Agent);
|
||||||
msg.setSentDate(new Date());
|
msg.setSentDate(new Date());
|
||||||
msg.setFrom(sender);
|
msg.setFrom(sender);
|
||||||
msg.setRecipients(Message.RecipientType.TO, recipient);
|
msg.setRecipients(Message.RecipientType.TO, recipient);
|
||||||
|
Reference in New Issue
Block a user