implemented:
- altering of mail settings - sending email Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -13,6 +13,7 @@ dependencies {
|
|||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
implementation 'org.json:json:20240303'
|
implementation 'org.json:json:20240303'
|
||||||
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
|
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
|
||||||
|
implementation 'com.sun.mail:jakarta.mail:2.0.1'
|
||||||
implementation project(':de.srsoftware.utils')
|
implementation project(':de.srsoftware.utils')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ public class Constants {
|
|||||||
public static final String RESPONSE_TYPE = "response_type";
|
public static final String RESPONSE_TYPE = "response_type";
|
||||||
public static final String SCOPE = "scope";
|
public static final String SCOPE = "scope";
|
||||||
public static final String SECRET = "secret";
|
public static final String SECRET = "secret";
|
||||||
public static final String SENDER_ADDRESS = "sender_address";
|
public static final String SMTP_USER = "smtp_user";
|
||||||
|
public static final String SMTP_PASSWORD = "smtp_pass";
|
||||||
public static final String SMTP_AUTH = "smtp_auth";
|
public static final String SMTP_AUTH = "smtp_auth";
|
||||||
public static final String SMTP_HOST = "smtp_host";
|
public static final String SMTP_HOST = "smtp_host";
|
||||||
public static final String SMTP_PORT = "smtp_port";
|
public static final String SMTP_PORT = "smtp_port";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.srsoftware.oidc.api;
|
|||||||
|
|
||||||
import static de.srsoftware.oidc.api.Constants.*;
|
import static de.srsoftware.oidc.api.Constants.*;
|
||||||
|
|
||||||
|
import jakarta.mail.Authenticator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
@@ -35,15 +36,20 @@ public interface MailConfig {
|
|||||||
props.put("mail.smtp.port", smtpPort());
|
props.put("mail.smtp.port", smtpPort());
|
||||||
props.put("mail.smtp.auth", smtpAuth() ? "true" : "false");
|
props.put("mail.smtp.auth", smtpAuth() ? "true" : "false");
|
||||||
props.put("mail.smtp.starttls.enable", startTls() ? "true" : "false");
|
props.put("mail.smtp.starttls.enable", startTls() ? "true" : "false");
|
||||||
|
props.put("mail.smtp.ssl.trust", smtpHost());
|
||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
default Map<String, Object> map() {
|
default Map<String, Object> map() {
|
||||||
return Map.of( //
|
return Map.of( //
|
||||||
SMTP_HOST, smtpHost(), //
|
SMTP_HOST, smtpHost(), //
|
||||||
SMTP_PORT, smtpPort(), //
|
SMTP_PORT, smtpPort(), //
|
||||||
SMTP_AUTH, smtpAuth(), //
|
SMTP_AUTH, smtpAuth(), //
|
||||||
SENDER_ADDRESS, senderAddress(), //
|
SMTP_USER, senderAddress(), //
|
||||||
START_TLS, startTls());
|
START_TLS, startTls());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Authenticator authenticator();
|
||||||
|
|
||||||
|
MailConfig save();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.backend;
|
package de.srsoftware.oidc.backend;
|
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.Constants.*;
|
||||||
import static de.srsoftware.oidc.api.data.Permission.MANAGE_SMTP;
|
import static de.srsoftware.oidc.api.data.Permission.MANAGE_SMTP;
|
||||||
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
|
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
|
||||||
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||||
@@ -31,8 +32,33 @@ public class EmailController extends Controller {
|
|||||||
return notFound(ex);
|
return notFound(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||||
|
var optSession = getSession(ex);
|
||||||
|
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
||||||
|
var user = optSession.get().user();
|
||||||
|
switch (path) {
|
||||||
|
case "/settings":
|
||||||
|
return saveSettings(ex, user);
|
||||||
|
}
|
||||||
|
return notFound(ex);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean provideSettings(HttpExchange ex, User user) throws IOException {
|
private boolean provideSettings(HttpExchange ex, User user) throws IOException {
|
||||||
if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
|
if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
|
||||||
return sendContent(ex, mailConfig.map());
|
return sendContent(ex, mailConfig.map());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean saveSettings(HttpExchange ex, User user) throws IOException {
|
||||||
|
if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
|
||||||
|
var data = json(ex);
|
||||||
|
if (data.has(SMTP_HOST)) mailConfig.smtpHost(data.getString(SMTP_HOST));
|
||||||
|
if (data.has(SMTP_PORT)) mailConfig.smtpPort(data.getInt(SMTP_PORT));
|
||||||
|
if (data.has(SMTP_USER)) mailConfig.senderAddress(data.getString(SMTP_USER));
|
||||||
|
if (data.has(SMTP_PASSWORD)) mailConfig.senderPassword(data.getString(SMTP_PASSWORD));
|
||||||
|
if (data.has(SMTP_AUTH)) mailConfig.smtpAuth(data.getBoolean(SMTP_AUTH));
|
||||||
|
if (data.has(START_TLS)) mailConfig.startTls(data.getBoolean(START_TLS));
|
||||||
|
mailConfig.save();
|
||||||
|
return sendContent(ex, "saved");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,20 +20,13 @@ import java.util.Optional;
|
|||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class UserController extends Controller {
|
public class UserController extends Controller {
|
||||||
private final UserService users;
|
private final UserService users;
|
||||||
private final MailConfig mailConfig;
|
private final MailConfig mailConfig;
|
||||||
private final Authenticator auth;
|
|
||||||
|
|
||||||
public UserController(MailConfig mailConfig, SessionService sessionService, UserService userService) {
|
public UserController(MailConfig mailConfig, SessionService sessionService, UserService userService) {
|
||||||
super(sessionService);
|
super(sessionService);
|
||||||
users = userService;
|
users = userService;
|
||||||
this.mailConfig = mailConfig;
|
this.mailConfig = mailConfig;
|
||||||
auth = new Authenticator() {
|
|
||||||
// override the getPasswordAuthentication method
|
|
||||||
protected PasswordAuthentication getPasswordAuthentication() {
|
|
||||||
return new PasswordAuthentication(mailConfig.senderAddress(), mailConfig.senderPassword());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean addUser(HttpExchange ex, Session session) throws IOException {
|
private boolean addUser(HttpExchange ex, Session session) throws IOException {
|
||||||
@@ -127,7 +120,7 @@ public class UserController extends Controller {
|
|||||||
private void senPasswordLink(User user) {
|
private void senPasswordLink(User user) {
|
||||||
LOG.log(WARNING, "Sending password link to {0}", user.email());
|
LOG.log(WARNING, "Sending password link to {0}", user.email());
|
||||||
try {
|
try {
|
||||||
var session = jakarta.mail.Session.getDefaultInstance(mailConfig.props(), auth);
|
var session = jakarta.mail.Session.getDefaultInstance(mailConfig.props(), mailConfig.authenticator());
|
||||||
Message message = new MimeMessage(session);
|
Message message = new MimeMessage(session);
|
||||||
message.setFrom(new InternetAddress(mailConfig.senderAddress()));
|
message.setFrom(new InternetAddress(mailConfig.senderAddress()));
|
||||||
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(user.email()));
|
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(user.email()));
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencies {
|
|||||||
implementation project(':de.srsoftware.utils')
|
implementation project(':de.srsoftware.utils')
|
||||||
implementation 'org.json:json:20240303'
|
implementation 'org.json:json:20240303'
|
||||||
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
|
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
|
||||||
|
implementation 'com.sun.mail:jakarta.mail:2.0.1'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import static java.util.Optional.empty;
|
|||||||
|
|
||||||
import de.srsoftware.oidc.api.*;
|
import de.srsoftware.oidc.api.*;
|
||||||
import de.srsoftware.oidc.api.data.*;
|
import de.srsoftware.oidc.api.data.*;
|
||||||
|
import jakarta.mail.Authenticator;
|
||||||
|
import jakarta.mail.PasswordAuthentication;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -37,6 +39,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
private Map<String, Client> clients = new HashMap<>();
|
private Map<String, Client> clients = new HashMap<>();
|
||||||
private Map<String, User> accessTokens = new HashMap<>();
|
private Map<String, User> accessTokens = new HashMap<>();
|
||||||
private Map<String, Authorization> authCodes = new HashMap<>();
|
private Map<String, Authorization> authCodes = new HashMap<>();
|
||||||
|
private Authenticator auth;
|
||||||
|
|
||||||
public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException {
|
public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException {
|
||||||
this.storageFile = storage.toPath();
|
this.storageFile = storage.toPath();
|
||||||
@@ -48,9 +51,10 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
Files.writeString(storageFile, "{}");
|
Files.writeString(storageFile, "{}");
|
||||||
}
|
}
|
||||||
json = new JSONObject(Files.readString(storageFile));
|
json = new JSONObject(Files.readString(storageFile));
|
||||||
|
auth = null; // lazy init!
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileStore save() {
|
public FileStore save() {
|
||||||
try {
|
try {
|
||||||
Files.writeString(storageFile, json.toString(2));
|
Files.writeString(storageFile, json.toString(2));
|
||||||
return this;
|
return this;
|
||||||
@@ -319,6 +323,19 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
|
|
||||||
/*** MailConfig implementation ***/
|
/*** MailConfig implementation ***/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator authenticator() {
|
||||||
|
if (auth == null) {
|
||||||
|
auth = new Authenticator() {
|
||||||
|
// override the getPasswordAuthentication method
|
||||||
|
protected PasswordAuthentication getPasswordAuthentication() {
|
||||||
|
return new PasswordAuthentication(senderAddress(), senderPassword());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
private String mailConfig(String key) {
|
private String mailConfig(String key) {
|
||||||
var config = json.getJSONObject(MAILCONFIG);
|
var config = json.getJSONObject(MAILCONFIG);
|
||||||
if (config.has(key)) return config.getString(key);
|
if (config.has(key)) return config.getString(key);
|
||||||
@@ -328,71 +345,71 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
private FileStore mailConfig(String key, Object newValue) {
|
private FileStore mailConfig(String key, Object newValue) {
|
||||||
var config = json.getJSONObject(MAILCONFIG);
|
var config = json.getJSONObject(MAILCONFIG);
|
||||||
config.put(key, newValue);
|
config.put(key, newValue);
|
||||||
|
auth = null;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String smtpHost() {
|
public String smtpHost() {
|
||||||
return mailConfig("smtp_host");
|
return mailConfig(SMTP_HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MailConfig smtpHost(String newValue) {
|
public MailConfig smtpHost(String newValue) {
|
||||||
return mailConfig("smtp_host", newValue);
|
return mailConfig(SMTP_HOST, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int smtpPort() {
|
public int smtpPort() {
|
||||||
try {
|
var config = json.getJSONObject(MAILCONFIG);
|
||||||
return Integer.parseInt(mailConfig("smtp_port"));
|
return config.has(SMTP_PORT) ? config.getInt(SMTP_PORT) : 0;
|
||||||
} catch (NumberFormatException nfe) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MailConfig smtpPort(int newValue) {
|
public MailConfig smtpPort(int newValue) {
|
||||||
return mailConfig("smtp_port", newValue);
|
return mailConfig(SMTP_PORT, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String senderAddress() {
|
public String senderAddress() {
|
||||||
return mailConfig("sender_address");
|
return mailConfig(SMTP_USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MailConfig senderAddress(String newValue) {
|
public MailConfig senderAddress(String newValue) {
|
||||||
return mailConfig("sender_address", newValue);
|
return mailConfig(SMTP_USER, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String senderPassword() {
|
public String senderPassword() {
|
||||||
return mailConfig("smtp_password");
|
return mailConfig(SMTP_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MailConfig senderPassword(String newValue) {
|
public MailConfig senderPassword(String newValue) {
|
||||||
return mailConfig("smtp_password", newValue);
|
return mailConfig(SMTP_PASSWORD, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean startTls() {
|
public boolean startTls() {
|
||||||
return "true".equals(mailConfig("start_tls"));
|
var config = json.getJSONObject(MAILCONFIG);
|
||||||
|
return config.has(START_TLS) ? config.getBoolean(START_TLS) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MailConfig startTls(boolean newValue) {
|
public MailConfig startTls(boolean newValue) {
|
||||||
return mailConfig("start_tls", newValue);
|
return mailConfig(START_TLS, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean smtpAuth() {
|
public boolean smtpAuth() {
|
||||||
return "true".equals(mailConfig("smtp_auth"));
|
var config = json.getJSONObject(MAILCONFIG);
|
||||||
|
return config.has(SMTP_AUTH) ? config.getBoolean(SMTP_AUTH) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MailConfig smtpAuth(boolean newValue) {
|
public MailConfig smtpAuth(boolean newValue) {
|
||||||
return mailConfig("smtp_auth", newValue);
|
return mailConfig(SMTP_AUTH, newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ function hide(id){
|
|||||||
get(id).style.display = 'none';
|
get(id).style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChecked(id){
|
||||||
|
return get(id).checked;
|
||||||
|
}
|
||||||
|
|
||||||
function login(){
|
function login(){
|
||||||
redirect('login.html?return_to='+encodeURIComponent(window.location.href));
|
redirect('login.html?return_to='+encodeURIComponent(window.location.href));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,24 @@ async function handlePasswordResponse(response){
|
|||||||
},10000);
|
},10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSmtpResponse(response){
|
||||||
|
if (response.ok){
|
||||||
|
hide('wrong_password');
|
||||||
|
hide('password_mismatch');
|
||||||
|
setText('smtpBtn', 'saved.');
|
||||||
|
} else {
|
||||||
|
setText('smtpBtn', 'Update failed!');
|
||||||
|
var text = await response.text();
|
||||||
|
if (text == 'wrong password') show('wrong_password');
|
||||||
|
if (text == 'password mismatch') show('password_mismatch');
|
||||||
|
|
||||||
|
}
|
||||||
|
setTimeout(function(){
|
||||||
|
enable('smtpBtn');
|
||||||
|
setText('smtpBtn','Update');
|
||||||
|
},10000);
|
||||||
|
}
|
||||||
|
|
||||||
function handleResponse(response){
|
function handleResponse(response){
|
||||||
if (response.ok){
|
if (response.ok){
|
||||||
hide('update_error')
|
hide('update_error')
|
||||||
@@ -36,8 +54,8 @@ function handleResponse(response){
|
|||||||
show('update_error');
|
show('update_error');
|
||||||
setText('updateBtn', 'Update failed!');
|
setText('updateBtn', 'Update failed!');
|
||||||
}
|
}
|
||||||
enable('updateBtn');
|
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
|
enable('updateBtn');
|
||||||
setText('updateBtn','Update');
|
setText('updateBtn','Update');
|
||||||
},10000);
|
},10000);
|
||||||
}
|
}
|
||||||
@@ -49,6 +67,8 @@ async function handleSettings(response){
|
|||||||
for (var key in json){
|
for (var key in json){
|
||||||
setValue(key,json[key]);
|
setValue(key,json[key]);
|
||||||
}
|
}
|
||||||
|
get('start_tls').checked = json.start_tls;
|
||||||
|
get('smtp_auth').checked = json.smtp_auth;
|
||||||
show('mail_settings');
|
show('mail_settings');
|
||||||
} else {
|
} else {
|
||||||
hide('mail_settings');
|
hide('mail_settings');
|
||||||
@@ -59,6 +79,25 @@ function passKeyDown(ev){
|
|||||||
if (event.keyCode == 13) updatePass();
|
if (event.keyCode == 13) updatePass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSmtp(){
|
||||||
|
disable('smtpBtn');
|
||||||
|
var newData = {
|
||||||
|
smtp_host : getValue('smtp_host'),
|
||||||
|
smtp_port : getValue('smtp_port'),
|
||||||
|
smtp_user : getValue('smtp_user'),
|
||||||
|
smtp_pass : getValue('smtp_pass'),
|
||||||
|
smtp_auth : isChecked('smtp_auth'),
|
||||||
|
start_tls : isChecked('start_tls')
|
||||||
|
}
|
||||||
|
fetch("/api/email/settings",{
|
||||||
|
method : 'POST',
|
||||||
|
headers : {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body : JSON.stringify(newData)
|
||||||
|
}).then(handleSmtpResponse);
|
||||||
|
setText('smtpBtn','sent…');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function updatePass(){
|
function updatePass(){
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<br/>
|
||||||
<fieldset id="mail_settings" style="display: none">
|
<fieldset id="mail_settings" style="display: none">
|
||||||
<legend>
|
<legend>
|
||||||
Mail settings
|
Mail settings
|
||||||
@@ -85,20 +86,32 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Smtp host</th>
|
<th>Smtp host</th>
|
||||||
<td><input type="text" id="smtp_host"></td>
|
<td><input type="text" id="smtp_host" placeholder="smtp host"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Smtp port</th>
|
<th>Smtp port</th>
|
||||||
<td><input type="text" id="smtp_port"></td>
|
<td><input type="number" id="smtp_port"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Sender email address</th>
|
<th>Smtp user</th>
|
||||||
<td><input type="text" id="sender_mail"></td>
|
<td><input type="text" id="smtp_user" placeholder="smtp user"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Sender password</th>
|
<th>Smtp password</th>
|
||||||
<td><input type="password" id="sender_password"></td>
|
<td><input type="password" id="smtp_pass" placeholder="password"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Security</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="smtp_auth"> Auth
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="start_tls"> StartTLS
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><button id="smtpBtn" type="button" onClick="updateSmtp()">Update</button></td>
|
<td><button id="smtpBtn" type="button" onClick="updateSmtp()">Update</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user