implemented:

- altering of mail settings
- sending email

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2024-08-09 00:27:32 +02:00
parent f3c4c098c0
commit 31afced7f7
10 changed files with 141 additions and 40 deletions

View File

@@ -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')
} }

View File

@@ -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";

View File

@@ -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();
} }

View File

@@ -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");
}
} }

View File

@@ -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()));

View File

@@ -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'
} }

View File

@@ -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);
} }
} }

View File

@@ -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));
} }

View File

@@ -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(){

View File

@@ -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>