Compare commits

..

No commits in common. 'main' and 'drop_old_mail' have entirely different histories.

  1. 2
      Dockerfile
  2. 39
      README.md
  3. 85
      doc/anleitung.md
  4. 2
      pom.xml
  5. 93
      src/main/java/de/srsoftware/widerhall/Configuration.java
  6. 4
      src/main/java/de/srsoftware/widerhall/Constants.java
  7. 172
      src/main/java/de/srsoftware/widerhall/Util.java
  8. 80
      src/main/java/de/srsoftware/widerhall/data/Database.java
  9. 88
      src/main/java/de/srsoftware/widerhall/data/ListMember.java
  10. 143
      src/main/java/de/srsoftware/widerhall/data/MailingList.java
  11. 52
      src/main/java/de/srsoftware/widerhall/data/Post.java
  12. 61
      src/main/java/de/srsoftware/widerhall/data/User.java
  13. 125
      src/main/java/de/srsoftware/widerhall/mail/ImapClient.java
  14. 3
      src/main/java/de/srsoftware/widerhall/mail/ProblemListener.java
  15. 45
      src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java
  16. 125
      src/main/java/de/srsoftware/widerhall/web/Rest.java
  17. 2
      src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java
  18. 277
      src/main/java/de/srsoftware/widerhall/web/Web.java
  19. 65
      src/test/java/de/srsoftware/widerhall/UtilTest.java
  20. 4
      src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java
  21. 3
      static/templates/archive.st
  22. 8
      static/templates/footer.st
  23. 53
      static/templates/js.st
  24. 7
      static/templates/navigation.st
  25. 33
      static/templates/new_password_form.st
  26. 21
      static/templates/reset-pw.st
  27. 17
      static/templates/reset_link_sent.st
  28. 4
      static/templates/subscribe.st

2
Dockerfile

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
FROM alpine:3.16
FROM alpine:3.15
MAINTAINER Stephan Richter <s.richter@srsoftware.de>
RUN apk add --no-cache --update bash git maven openjdk17

39
README.md

@ -1,42 +1,3 @@ @@ -1,42 +1,3 @@
# Willkommen bei Widerhall!
(english version below)
## Was ist Widerhall?
Diese Software zielt darauf ab, _Mailing-Listen_ zu erstellen, ohne sich mit dem Thema _Mail-Server_ beschäftigen zu müssen.
Alles was du brauchst, um mit _Widerhall_ eine Mailing-Liste zu erstellen ist diese Software und eine IMAP-fähige Email-Adresse.
## Wie funktioniert das?
Nach dem Start der Software geht man auf deren Startseite und legt einen Administrations-Account an.
Dann kann man – als Admin – Mailing-Listen anlegen, indem man einfach die Zugangsdaten zu den entsprechenden Mail-Adressen in dieser Software hinterlegt.
Sobald man eine so angelegte Verteiler-Liste aktiviert, verbindet sich diese Software mit der entsprechenden Mailbox und wartet auf eingehende Nachrichten. Immer wenn eine Nachricht empfangen wird, wird diese an die jeweiligen Abonnenten der Liste weitergeleitet.
## Wie kann die Mailing-Liste abonniert werden?
Du kannst jede Mailing-Liste, welche du angelegt hast, öffentlich machen.
Alle öffentlichen Mailing-Listen werden auf der Startseite von _Widerhall_ aufgelistet. Besucher dieser Seite können dann jede beliebige der öffentlichen Mailing-Listen abonnieren, indem sie den **abonnieren**-Button klicken. Sie müssen dann ihren Namen und ihre Email-Adresse angeben. Danach bekommen Sie eine Email mit einem Link, den sie einmalig anklicken müssen, um ihr Abonnement zu bestätigen.
## Kostet es was?
Nein. _Widerhall_ ist FOSS. Das steht für Free Open Source Software – freie, quelloffene Software – und bedeutet jeder kann eine Kopie des Software-Codes beziehen und es selbstverantwortlich einsetzen.
## Anleitung
Lesen Sie die [Anleitung] für Hinweise zu Installation und Wartung.
## aktueller Status
Auch wenn diese Software schon einige Zeit für die Verteiler-Listen des Jenaer Reparier-Cafes verwendet wird, gibt es keine Garantie. Die Software könnte Fehler enthalten. Betrachten Sie die aktuelle Version als Beta.
[Anleitung]: doc/anleitung.md
-------
# Welcome to Widerhall!
## What is Widerhall?

85
doc/anleitung.md

@ -1,85 +0,0 @@ @@ -1,85 +0,0 @@
# Installation
## Docker
Die einfachste Art, Widerhall einzusetzen, ist Docker zu verwenden.
Wenn du Docker schon am Laufen hast musst du nur noch folgendes tun:
1. [Dockerfile](../Dockerfile) herunterladen
2. Image erstellen: `docker build -t widerhall /path-to-dockerfile/`
3. Container starten:
```bash
docker run \
--name widerhall \
-d widerhall
```
Danach solltest du zu http://<your machine>/ navigieren können.
### Konfiguration, Daten-Persistenz
Um Daten über Computer/Docker-Neustarts hinaus zu erhalten, muss ein _Volume_ in den Container eingebunden werden:
```bash
docker run \
--name widerhall \
-v /some/directory:/data
-d widerhall
```
_Widerhall_ wird alle zu speichernden Daten in `/data`, also dem eingebundenen Speicher, ablegen.
* `widerhall.sqlite3` – dies ist die Haupt-Datenbank-Datei. Passen Sie gut darauf auf!
* `widerhall.config.json` – dies ist die Haupt-Konfigurations-Datei. Dort können Sie die vorgegebenen Einstellungen überschreiben.
* `archive` – Hier werden gespeicherte Emails abgelegt.
Diese Dateien/Verzeichnisse werden beim ersten Start (oder wenn Sie zwischenzeitlich gelöscht wurden) angelegt.
Die Konfiguration ist wie folgt aufgebaut:
```json
{
"port":80,
"base_url":"https://widerhall.srsoftware.de",
"locations":{
"database":"/data/widerhall.sqlite3",
"configuration":"/data/widerhall.config.json",
"archive":"/data/archive",
"base":"/Widerhall"
}
}
```
* Die `base_url` sollte so angepasst werden, dass sie Ihren Server-Einstellungen entspricht.
* Falls Sie die Datenbankdatei an einem anderen Ort speichern wollen, ändern Sie den `database`-Eintrag.
* Wenn Sie die `configuration`-Einstellung überschreiben, wird _Widerhall_ seine Konfiguration ersetzen mit der Datei, die an der angegebenen Stelle gefunden wird.
* Falls das Mail-Archiv an einem anderen Ort gespeichert werden soll, kann der `archive`-Eintrag angepasst werden.
* `base` sollte auf das Installations-Verzeichnis von _Widerhall_ zeigen. Standardmäßig ist das _/Widerhall_.
### SSL termination
_Widerhall_ versucht alles einfach zu halten. Der eingebaute Webserver hat keine Ahnung von SSL.
Wenn Sie die Website sicher betreiben wollen, lassen Sie _Widerhall_ hintereinem **reverse proxy**, wie z.B. [nginx_proxy] laufen.
## Maven
_Widerhall_ ist ein Java-Project, welches mit Apache Maven gebaut werden kann.
Fall Sie eine aktuelle Java-Installation (Java 17+) und Maven auf Ihrem System haben, sollten Sie _Widerhall_ wie folgt compilieren können:
1. Holen Sie sich den Programm-Code: `git clone https://git.srsoftware.de/StephanRichter/Widerhall.git`
2. Wechseln Sie ins heruntergeladene Verzeichnis: `cd Widerhall`
3. Erzeugen Sie das ausführbare JAR-Archiv: `mvn clean test compile assembly:single`
4. Starten Sie das JAR: `java -jar target/*.jar`
# Übersetzungen
Die Übersetzungen von _Widerhall_ werden als Branches des Quellcodes gepflegt:
* _main_-Branch: english
* _lang_de_-Branch: deutsch
Einfach die Sprache Ihrer Wahl auschecken.
[nginx_proxy]: https://github.com/nginx-proxy/nginx-proxy

2
pom.xml

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
<groupId>org.example</groupId>
<artifactId>Widerhall</artifactId>
<version>1.0.7</version>
<version>0.2.55</version>
<build>
<plugins>
<plugin>

93
src/main/java/de/srsoftware/widerhall/Configuration.java

@ -17,45 +17,6 @@ public class Configuration { @@ -17,45 +17,6 @@ public class Configuration {
private JSONObject data;
private File file;
public File archiveDir() {
var locations = locations();
if (!locations.containsKey(ARCHIVE)) locations.put(ARCHIVE, String.join(File.separator,baseDir(),"archive"));
return new File((String) locations.get(ARCHIVE));
}
public String baseDir() {
var locations = locations();
if (!locations.containsKey(BASE)) locations.put(BASE,System.getProperty("user.dir"));
return (String) locations.get(BASE);
}
public String baseUrl() {
if (!data.containsKey(BASE_URL)) data.put(BASE_URL,"http://localhost");
return (String) data.get(BASE_URL);
}
public File configFile() {
var locations = locations();
if (!locations.containsKey(CONFIG)) locations.put(CONFIG, String.join(File.separator,baseDir(),"config","config.json"));
return new File((String) locations.get(CONFIG));
}
public File dbFile() {
var locations = locations();
if (!locations.containsKey(DB)) locations.put(DB, String.join(File.separator,baseDir(),"db","db.sqlite3"));
return new File((String) locations.get(DB));
}
public Configuration dbFile(File dbFile){
var locations = locations();
locations.put(DB,dbFile.toString());
return this;
}
public File file() {
return file;
}
public static Configuration instance() {
if (singleton == null) singleton = new Configuration().setDefaults();
return singleton;
@ -81,12 +42,6 @@ public class Configuration { @@ -81,12 +42,6 @@ public class Configuration {
return this;
}
private JSONObject locations() {
Object o = data.get(LOCATIONS);
if (!(o instanceof JSONObject)) data.put(LOCATIONS,o = new JSONObject());
return (JSONObject) o;
}
public Configuration save(File file) throws IOException {
this.file = file;
return save();
@ -109,9 +64,57 @@ public class Configuration { @@ -109,9 +64,57 @@ public class Configuration {
return this;
}
private JSONObject locations() {
Object o = data.get(LOCATIONS);
if (!(o instanceof JSONObject)) data.put(LOCATIONS,o = new JSONObject());
return (JSONObject) o;
}
public String baseDir() {
var locations = locations();
if (!locations.containsKey(BASE)) locations.put(BASE,System.getProperty("user.dir"));
return (String) locations.get(BASE);
}
public File configFile() {
var locations = locations();
if (!locations.containsKey(CONFIG)) locations.put(CONFIG, String.join(File.separator,baseDir(),"config","config.json"));
return new File((String) locations.get(CONFIG));
}
public File dbFile() {
var locations = locations();
if (!locations.containsKey(DB)) locations.put(DB, String.join(File.separator,baseDir(),"db","db.sqlite3"));
return new File((String) locations.get(DB));
}
public Configuration dbFile(File dbFile){
var locations = locations();
locations.put(DB,dbFile.toString());
return this;
}
public File archiveDir() {
var locations = locations();
if (!locations.containsKey(ARCHIVE)) locations.put(ARCHIVE, String.join(File.separator,baseDir(),"archive"));
return new File((String) locations.get(ARCHIVE));
}
public int serverPort() {
if (!data.containsKey(PORT)) data.put(PORT,80L);
var o = data.get(PORT);
return (int) (long) o;
}
public String baseUrl() {
if (!data.containsKey(BASE_URL)) data.put(BASE_URL,"http://localhost");
return (String) data.get(BASE_URL);
}
public File file() {
return file;
}
}

4
src/main/java/de/srsoftware/widerhall/Constants.java

@ -18,13 +18,9 @@ public class Constants { @@ -18,13 +18,9 @@ public class Constants {
public static final String LIST = "list";
public static final String LOCATIONS = "locations";
public static final String NAME = "name";
public static final String MESSAGE_ID = "message_id";
public static final String MODERATOR = "moderator";
public static final String MONTH = "month";
public static final String NEW_PASSWORD_FORM = "new_password_form";
public static final String NOTES = "notes";
public static final String PASSWORD = "password";
public static final String PASSWORD_REPEAT = "password-repeat";
public static final String PERMISSIONS = "permissions";
public static final Object PORT = "port";
public static final String PREFIX = "prefix";

172
src/main/java/de/srsoftware/widerhall/Util.java

@ -4,19 +4,19 @@ import de.srsoftware.tools.translations.Translation; @@ -4,19 +4,19 @@ import de.srsoftware.tools.translations.Translation;
import de.srsoftware.widerhall.data.MailingList;
import de.srsoftware.widerhall.data.User;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import static de.srsoftware.widerhall.Constants.*;
@ -26,41 +26,19 @@ public class Util { @@ -26,41 +26,19 @@ public class Util {
private static final MessageDigest SHA256 = getSha256();
private static final String EMAIL_PATTERN = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+(?:\\.[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+)*@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$";
public static char boundedChar(int i) {
i = (i<0 ? -i : i) % 62;
i += '0';
if (i>57) i+=7;
if (i>90) i+=6;
return (char) i;
}
public static String dropEmail(String tx) {
return tx.replaceAll( "[.\\-\\w]+@[.\\-\\w]+", "[email_removed]");
public static String urlEncode(Map<String, Object> data) {
String params = data.entrySet()
.stream()
.map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
.collect(Collectors.joining("&"));
return params;
}
private static String encode(Object value) {
return URLEncoder.encode(value.toString(), StandardCharsets.UTF_8);
}
public static boolean getCheckbox(HttpServletRequest req, String key) {
return "on".equals(req.getParameter(key));
}
public static MailingList getMailingList(HttpServletRequest req) {
var listEmail = req.getParameter(LIST);
if (listEmail == null || listEmail.isBlank()) return null;
return MailingList.load(listEmail);
}
public static <T> T getNullable(ResultSet rs, String colName) throws SQLException {
final T val = (T) rs.getObject(colName);
return rs.wasNull() ? null : val;
}
public static String getPath(HttpServletRequest req) {
var path = req.getPathInfo();
return path == null ? INDEX : path.substring(1);
}
public static MessageDigest getSha256() {
try {
return MessageDigest.getInstance("SHA-256");
@ -70,43 +48,9 @@ public class Util { @@ -70,43 +48,9 @@ public class Util {
}
}
/**
* Return the primary text content of the message.
*/
public static String getText(Part p) throws MessagingException, IOException {
// https://javaee.github.io/javamail/FAQ
if (p.isMimeType("text/*")) return (String)p.getContent();
if (p.isMimeType("multipart/alternative")) {
// prefer html text over plain text
Multipart mp = (Multipart)p.getContent();
String text = null;
for (int i = 0; i < mp.getCount(); i++) {
Part bp = mp.getBodyPart(i);
if (bp.isMimeType("text/plain")) {
if (text == null) text = getText(bp);
continue;
} else if (bp.isMimeType("text/html")) {
String s = getText(bp);
if (s != null) return s;
} else {
return getText(bp);
}
}
return text;
} else if (p.isMimeType("multipart/*")) {
Multipart mp = (Multipart)p.getContent();
for (int i = 0; i < mp.getCount(); i++) {
String s = getText(mp.getBodyPart(i));
if (s != null) return s;
}
}
return null;
}
public static User getUser(HttpServletRequest req) {
var o = req.getSession().getAttribute(USER);
return o instanceof User ? (User) o : null;
public static String sha256(String s) {
byte[] bytes = SHA256.digest(s.getBytes(StandardCharsets.UTF_8));
return hex(bytes);
}
private static String hex(byte[] bytes) {
@ -121,25 +65,12 @@ public class Util { @@ -121,25 +65,12 @@ public class Util {
return (char)(upper < 10 ? '0'+upper : 'A'+upper-10)+""+(char)(lower < 10 ? '0'+lower : 'A'+lower-10);
}
public static boolean isEmail(String email) {
return email.matches(EMAIL_PATTERN);
}
public static String randomString(int length) {
Random rand = new Random();
StringBuilder sb = new StringBuilder();
for (int i=0; i<length; i++) {
int k = rand.nextInt();
char c = boundedChar(k);
System.out.println("adding '"+c+"'…");
sb.append(c);
}
return sb.toString();
public static String t(String tx, Object ... fills){
return Translation.get(Application.class,tx,fills);
}
public static String sha256(String s) {
byte[] bytes = SHA256.digest(s.getBytes(StandardCharsets.UTF_8));
return hex(bytes);
public static boolean isEmail(String email) {
return email.matches(EMAIL_PATTERN);
}
public static boolean simplePassword(String pass) {
@ -165,23 +96,70 @@ public class Util { @@ -165,23 +96,70 @@ public class Util {
return false;
}
public static String t(String tx, Object ... fills){
return Translation.get(Application.class,tx,fills);
public static int unset(int value, int...flags) {
for (int flag : flags){
if ((value & flag) > 0) value ^= flag;
}
return value;
}
public static String urlEncode(Map<String, Object> data) {
String params = data.entrySet()
.stream()
.map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
.collect(Collectors.joining("&"));
return params;
public static User getUser(HttpServletRequest req) {
var o = req.getSession().getAttribute(USER);
return o instanceof User ? (User) o : null;
}
public static String getPath(HttpServletRequest req) {
var path = req.getPathInfo();
return path == null ? INDEX : path.substring(1);
public static int unset(int value, int...flags) {
for (int flag : flags){
if ((value & flag) > 0) value ^= flag;
}
public static MailingList getMailingList(HttpServletRequest req) {
var listEmail = req.getParameter(LIST);
if (listEmail == null || listEmail.isBlank()) return null;
return MailingList.load(listEmail);
}
public static boolean getCheckbox(HttpServletRequest req, String key) {
return "on".equals(req.getParameter(key));
}
/**
* Return the primary text content of the message.
*/
public static String getText(Part p) throws MessagingException, IOException {
// https://javaee.github.io/javamail/FAQ
if (p.isMimeType("text/*")) return (String)p.getContent();
if (p.isMimeType("multipart/alternative")) {
// prefer html text over plain text
Multipart mp = (Multipart)p.getContent();
String text = null;
for (int i = 0; i < mp.getCount(); i++) {
Part bp = mp.getBodyPart(i);
if (bp.isMimeType("text/plain")) {
if (text == null) text = getText(bp);
continue;
} else if (bp.isMimeType("text/html")) {
String s = getText(bp);
if (s != null) return s;
} else {
return getText(bp);
}
}
return text;
} else if (p.isMimeType("multipart/*")) {
Multipart mp = (Multipart)p.getContent();
for (int i = 0; i < mp.getCount(); i++) {
String s = getText(mp.getBodyPart(i));
if (s != null) return s;
}
}
return value;
return null;
}
public static String dropEmail(String tx) {
return tx.replaceAll( "[.\\-\\w]+@[.\\-\\w]+", "[email_removed]");
}
}

80
src/main/java/de/srsoftware/widerhall/data/Database.java

@ -10,7 +10,6 @@ import java.sql.ResultSet; @@ -10,7 +10,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Util.t;
import static de.srsoftware.widerhall.data.MailingList.HOLD_TIME;
@ -20,7 +19,6 @@ import static de.srsoftware.widerhall.data.MailingList.HOLD_TIME; @@ -20,7 +19,6 @@ import static de.srsoftware.widerhall.data.MailingList.HOLD_TIME;
*/
public class Database {
private static final Logger LOG = LoggerFactory.getLogger(Database.class);
private static final String DB_VERSION = "db_version";
private static Database singleton = null; // we only need one db handle ever. This will be it.
private final Connection conn; // the actual db connection handle within the singleton
@ -93,7 +91,6 @@ public class Database { @@ -93,7 +91,6 @@ public class Database {
*/
public class Request{
private String groupBy = null;
private final StringBuilder sql; // buffer the sql to be built
private final HashMap<String, List<Object>> where = new HashMap<>(); // buffer condition statements for select
private final HashMap<String,Object> values = new HashMap<>(); // buffer values for insert/update statements
@ -131,14 +128,33 @@ public class Database { @@ -131,14 +128,33 @@ public class Database {
}
}
private void applyGrouping(){
if (groupBy != null && !groupBy.isBlank()) sql.append(" GROUP BY ").append(groupBy.trim());
}
private void applySorting(){
if (!sortFields.isEmpty()) sql.append(" ORDER BY ").append(String.join(", ",sortFields));
}
@Override
protected Request clone() {
Request clone = new Request(new StringBuilder(sql));
clone.where.putAll(where);
clone.values.putAll(values);
return clone;
}
/**
* finalize sql, save sql and arguments as compiled request
* @return
*/
public CompiledRequest compile(Object ...additionalArgs){
var args = new ArrayList<>();
applyValues(args);
applyConditions(args);
applySorting();
if (additionalArgs != null) {
for (Object arg : additionalArgs) args.add(arg);
}
return new CompiledRequest(sql.toString(),args);
}
/**
* apply values (for insert or update statements)
* @param args
@ -171,34 +187,6 @@ public class Database { @@ -171,34 +187,6 @@ public class Database {
}
}
@Override
protected Request clone() {
Request clone = new Request(new StringBuilder(sql));
clone.where.putAll(where);
clone.values.putAll(values);
return clone;
}
/**
* finalize sql, save sql and arguments as compiled request
* @return
*/
public CompiledRequest compile(Object ...additionalArgs){
var args = new ArrayList<>();
applyValues(args);
applyConditions(args);
applyGrouping();
applySorting();
if (additionalArgs != null) {
for (Object arg : additionalArgs) args.add(arg);
}
return new CompiledRequest(sql.toString(),args);
}
public Request groupBy(String column) {
groupBy = column;
return this;
}
public void run() throws SQLException {
compile().run();
@ -297,14 +285,6 @@ public class Database { @@ -297,14 +285,6 @@ public class Database {
return this;
}
private void createVersionTable() throws SQLException {
var sql = "CREATE TABLE %s (%s %s NOT NULL PRIMARY KEY);".formatted(DB_VERSION,DB_VERSION,INT);
var db = Database.open();
db.query(sql).compile().run();
sql = "INSERT INTO %s VALUES (1)".formatted(DB_VERSION);
db.query(sql).compile().run();
}
private boolean columnExists(String tableName, String columnName) throws SQLException {
var rs = Database.open().select("pragma_table_info('"+tableName+"')","COUNT(*) AS num").where("name",columnName).compile().exec();
try {
@ -367,7 +347,6 @@ public class Database { @@ -367,7 +347,6 @@ public class Database {
try {
singleton = new Database(DriverManager.getConnection(url));
singleton.assertTables(); // must not be concatenated to exception above (assertTables accesses singleton)!
singleton.update202405();
} catch (SQLException sqle) {
sqle.printStackTrace();
}
@ -384,10 +363,6 @@ public class Database { @@ -384,10 +363,6 @@ public class Database {
return new Request(sql);
}
public Request query(String sql) {
return new Request(new StringBuilder(sql));
}
/**
* create a SELECT [flields] FROM [table] request.
* If no fields are supplied, a request in the form SELECT * FROM [table] will be generated.
@ -405,6 +380,7 @@ public class Database { @@ -405,6 +380,7 @@ public class Database {
return new Request(sql.append(" FROM ").append(tableName));
}
/**
* check, whether a table with the provided name exists
* @param tbName
@ -434,12 +410,4 @@ public class Database { @@ -434,12 +410,4 @@ public class Database {
public Request update(String tableName) {
return new Request(new StringBuilder("UPDATE ").append(tableName));
}
private Database update202405() throws SQLException {
if (!tableExists(Database.DB_VERSION)) {
createVersionTable();
User.addTokenColumn();
}
return this;
}
}

88
src/main/java/de/srsoftware/widerhall/data/ListMember.java

@ -50,13 +50,13 @@ public class ListMember { @@ -50,13 +50,13 @@ public class ListMember {
}
public String addNewModerator(String userEmail) {
if (!isAllowedToEditMods()) return t("You are not allowed to nominate new mods for {}!",list.email());
if (!isAllowedToEditMods()) return t("You are not allowed to nominate new mods for {}",list.email());
User moderator = null;
try {
moderator = User.load(userEmail);
} catch (SQLException e) {
LOG.warn("Failed to load user for {}!",userEmail,e);
return t("Failed to load user for {}!",userEmail);
LOG.warn("Failed to load user for {}",userEmail,e);
return t("Failed to load user for {}",userEmail);
}
if (moderator == null) return t("No such user: {}",userEmail);
@ -64,14 +64,14 @@ public class ListMember { @@ -64,14 +64,14 @@ public class ListMember {
try {
member = ListMember.load(list,moderator);
} catch (SQLException e) {
LOG.warn("Failed to load list member for {}/{}!",moderator.email(),list.email(),e);
return t("Failed to load list member for {}/{}!",moderator.email(),list.email());
LOG.warn("Failed to load list member for {}/{}",moderator.email(),list.email(),e);
return t("Failed to load list member for {}/{}",moderator.email(),list.email());
}
try {
if (member == null) {
ListMember.create(list, moderator, STATE_MODERATOR);
ListMember.create(list, moderator, ListMember.STATE_MODERATOR);
} else {
member.setState(Util.unset(member.state|STATE_MODERATOR,STATE_AWAITING_CONFIRMATION));
member.setState(ListMember.STATE_MODERATOR);
}
} catch (SQLException e) {
LOG.warn("Failed to make {} a moderator of {}",moderator.email(),list.email(),e);
@ -112,6 +112,20 @@ public class ListMember { @@ -112,6 +112,20 @@ public class ListMember {
return null;
}
public void sendConfirmationMail(ST template) throws SQLException, MessagingException {
var subject = t("[{}] Subscription complete!",list.name());
var data = new HashMap<String,Object>();
data.put(USER,user.safeMap());
data.put(LIST,list.minimalMap());
data.put(URL,Configuration.instance().baseUrl()+"/web/index");
if (list.isOpenForSubscribers()) data.put("open_list",true);
var text = template.add("data",data).render();
try {
list.smtp().send(list.email(),list.name(),user.email(),subject,text);
} catch (UnsupportedEncodingException e) {
LOG.warn("Failed to send list subscription confirmation!",e);
}
}
/**
* Create a new list member entry in the database.
* If the member has the state AWAITING_CONFIRMATION, a token is assigned with the member, too.
@ -146,13 +160,13 @@ public class ListMember { @@ -146,13 +160,13 @@ public class ListMember {
}
public String dropMember(String userEmail) {
if (!isModerator()) return t("You are not allowed to remove members of {}!",list.email());
if (!isModerator()) return t("You are not allowed to remove members of {}",list.email());
User user = null;
try {
user = User.load(userEmail);
} catch (SQLException e) {
LOG.warn("Failed to load user for {}!",userEmail,e);
return t("Failed to load user for {}!",userEmail);
LOG.warn("Failed to load user for {}",userEmail,e);
return t("Failed to load user for {}",userEmail);
}
if (user == null) return t("No such user: {}",userEmail);
@ -160,30 +174,30 @@ public class ListMember { @@ -160,30 +174,30 @@ public class ListMember {
try {
member = ListMember.load(list,user);
} catch (SQLException e) {
LOG.warn("Failed to load list member for {}/{}!",user.email(),list.email(),e);
return t("Failed to load list member for {}/{}!",user.email(),list.email());
LOG.warn("Failed to load list member for {}/{}",user.email(),list.email(),e);
return t("Failed to load list member for {}/{}",user.email(),list.email());
}
if (member == null) return t("{} is no member of {}!",user.email(),list.email());
if (member.isOwner()) return t("You are not allowed to remove the list owner!");
if (member == null) return t("{} is no member of {}",user.email(),list.email());
if (member.isOwner()) return t("You are not allowed to remvoe the list owner!");
try {
member.unsubscribe();
} catch (SQLException e) {
LOG.warn("Failed to un-subscribe {} from {}!",user.email(),list.email(),e);
return t("Failed to un-subscribe {} from {}!",user.email(),list.email());
LOG.warn("Failed to un-subscribe {} from {}",user.email(),list.email(),e);
return t("Failed to un-subscribe {} from {}",user.email(),list.email());
}
return null;
}
public String dropModerator(String userEmail) {
if (!isAllowedToEditMods()) return t("You are not allowed to edit mods of {}!",list.email());
if (!isAllowedToEditMods()) return t("You are not allowed to edit mods of {}",list.email());
User moderator = null;
try {
moderator = User.load(userEmail);
} catch (SQLException e) {
LOG.warn("Failed to load user for {}!",userEmail,e);
return t("Failed to load user for {}!",userEmail);
LOG.warn("Failed to load user for {}",userEmail,e);
return t("Failed to load user for {}",userEmail);
}
if (moderator == null) return t("No such user: {}",userEmail);
@ -191,18 +205,18 @@ public class ListMember { @@ -191,18 +205,18 @@ public class ListMember {
try {
member = ListMember.load(list,moderator);
} catch (SQLException e) {
LOG.warn("Failed to load list member for {}/{}!",moderator.email(),list.email(),e);
return t("Failed to load list member for {}/{}!",moderator.email(),list.email());
LOG.warn("Failed to load list member for {}/{}",moderator.email(),list.email(),e);
return t("Failed to load list member for {}/{}",moderator.email(),list.email());
}
try {
if (member == null) {
ListMember.create(list, moderator, ListMember.STATE_SUBSCRIBER);
} else {
member.setState(Util.unset(member.state,STATE_MODERATOR));
member.setState(ListMember.STATE_SUBSCRIBER);
}
} catch (SQLException e) {
LOG.warn("Failed to make {} a subscriber of {}!",moderator.email(),list.email(),e);
return t("Failed to make {} a subscriber of {}!",moderator.email(),list.email());
LOG.warn("Failed to make {} a subscriber of {}",moderator.email(),list.email(),e);
return t("Failed to make {} a subscriber of {}",moderator.email(),list.email());
}
return null;
}
@ -308,7 +322,6 @@ public class ListMember { @@ -308,7 +322,6 @@ public class ListMember {
* @throws SQLException
*/
public static ListMember load(MailingList list,User user) throws SQLException {
if (list == null || user == null) return null;
var rs = Database
.open()
.select(TABLE_NAME)
@ -360,21 +373,6 @@ public class ListMember { @@ -360,21 +373,6 @@ public class ListMember {
return this;
}
public void sendConfirmationMail(ST template) throws SQLException, MessagingException {
var subject = t("[{}] Subscription complete!",list.name());
var data = new HashMap<String,Object>();
data.put(USER,user.safeMap());
data.put(LIST,list.minimalMap());
data.put(URL,Configuration.instance().baseUrl()+"/web/index");
if (list.isOpenForSubscribers()) data.put("open_list",true);
var text = template.add("data",data).render();
try {
list.smtp().send(list.email(),list.name(),user.email(),subject,text);
} catch (UnsupportedEncodingException e) {
LOG.warn("Failed to send list subscription confirmation!",e);
}
}
public ListMember setState(int newState) throws SQLException {
Database.open()
.update(TABLE_NAME)
@ -391,11 +389,11 @@ public class ListMember { @@ -391,11 +389,11 @@ public class ListMember {
* @return
*/
public String stateText() {
var words = new TreeSet<String>();
if (isAwaiting()) words.add(t("awaiting confirmation"));
if (isModerator()) words.add(t("moderator"));
if (isOwner()) words.add(t("owner"));
if (isSubscriber()) words.add(t("subscriber"));
var words = new ArrayList<String>();
if (isAwaiting()) words.add("awaiting confirmation");
if (isModerator()) words.add("moderator");
if (isOwner()) words.add("owner");
if (isSubscriber()) words.add("subscriber");
return String.join(", ",words);
}

143
src/main/java/de/srsoftware/widerhall/data/MailingList.java

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
package de.srsoftware.widerhall.data;
import de.srsoftware.widerhall.Configuration;
import de.srsoftware.widerhall.Util;
import de.srsoftware.widerhall.mail.ImapClient;
import de.srsoftware.widerhall.mail.MessageHandler;
import de.srsoftware.widerhall.mail.ProblemListener;
@ -13,7 +12,8 @@ import org.stringtemplate.v4.ST; @@ -13,7 +12,8 @@ import org.stringtemplate.v4.ST;
import javax.mail.*;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import java.io.UnsupportedEncodingException;
import javax.xml.crypto.Data;
import java.io.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
@ -22,7 +22,6 @@ import java.util.stream.Stream; @@ -22,7 +22,6 @@ import java.util.stream.Stream;
import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Util.t;
import static de.srsoftware.widerhall.data.ListMember.*;
import static de.srsoftware.widerhall.data.User.PERMISSION_ADMIN;
/**
@ -50,8 +49,8 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -50,8 +49,8 @@ public class MailingList implements MessageHandler, ProblemListener {
private static final String SMTP_PASS = "smtp_pass";
public static final String TABLE_NAME = "Lists";
private static final int STATE_PENDING = 0;
public static final int STATE_ENABLED = 1; // do we process incoming messages?
public static final int STATE_PUBLIC = 2; // can guests see this ML?
private static final int STATE_ENABLED = 1; // do we process incoming messages?
private static final int STATE_PUBLIC = 2; // can guests see this ML?
public static final int STATE_FORWARD_FROM = 4; // set original sender as FROM when forwarding?
public static final int STATE_FORWARD_ATTACHED = 8; // forward messages as attachment?
public static final int STATE_HIDE_RECEIVERS = 16; // send using BCC receivers
@ -88,13 +87,12 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -88,13 +87,12 @@ public class MailingList implements MessageHandler, ProblemListener {
* @param smtpPass
* @param state
*/
public MailingList(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String inbox, String smtpHost, int smtpPort, String smtpUser, String smtpPass, int state, Integer holdTime) {
public MailingList(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String inbox, String smtpHost, int smtpPort, String smtpUser, String smtpPass, int state) {
this.email = email.toLowerCase();
this.name = name;
this.state = state;
this.smtp = new SmtpClient(smtpHost,smtpPort,smtpUser,smtpPass,email);
this.imap = new ImapClient(imapHost,imapPort,imapUser,imapPass,inbox,this);
this.holdTime = holdTime;
}
public MailingList archive(boolean enabled) throws SQLException {
@ -125,8 +123,8 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -125,8 +123,8 @@ public class MailingList implements MessageHandler, ProblemListener {
* @return
* @throws SQLException
*/
public static MailingList create(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String inbox, String smtpHost, int smtpPort, String smtpUser, String smtpPass, Integer holdTime) throws SQLException {
return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, DEFAULT_STATE, holdTime).save();
public static MailingList create(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String inbox, String smtpHost, int smtpPort, String smtpUser, String smtpPass) throws SQLException {
return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, DEFAULT_STATE).save();
}
public static void createHoldTimeColumn() throws SQLException {
@ -188,7 +186,7 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -188,7 +186,7 @@ public class MailingList implements MessageHandler, ProblemListener {
}
private void forward(Message message, Stream<ListMember> members) throws MessagingException {
if (hasPublicArchive()) storeMessage(message);
if (hasState(STATE_PUBLIC_ARCHIVE)) storeMessage(message);
String newSender = !hasState(STATE_FORWARD_FROM) ? email() : null;
var receivers = members
.map(ListMember::user)
@ -234,15 +232,10 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -234,15 +232,10 @@ public class MailingList implements MessageHandler, ProblemListener {
rs.getInt(SMTP_PORT),
rs.getString(SMTP_USER),
rs.getString(SMTP_PASS),
rs.getInt(STATE),
Util.getNullable(rs,HOLD_TIME)));
rs.getInt(STATE)));
return ml;
}
public boolean hasPublicArchive() {
return hasState(STATE_PUBLIC_ARCHIVE);
}
public boolean hasState(int test){
return (state & test) > 0;
}
@ -269,9 +262,9 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -269,9 +262,9 @@ public class MailingList implements MessageHandler, ProblemListener {
if (user == null) return false;
try {
var member = ListMember.load(this,user);
return member.hasState(STATE_OWNER|STATE_SUBSCRIBER); // owners may subscribe their own mailing lists
return member.hasState(ListMember.STATE_OWNER|ListMember.STATE_SUBSCRIBER); // owners may subscribe their own mailing lists
} catch (SQLException e) {
LOG.warn("Was not able to load ListMember:",e);
LOG.warn("Was not able to load ListMember: ",e);
return false;
}
}
@ -284,28 +277,6 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -284,28 +277,6 @@ public class MailingList implements MessageHandler, ProblemListener {
return hasState(STATE_OPEN_FOR_GUESTS|STATE_OPEN_FOR_SUBSCRIBERS);
}
public boolean isOwnedBy(User user) {
if (user == null) return false;
try {
if (ListMember.load(this,user).isOwner()) return true;
} catch (SQLException e) {
LOG.debug("Error loading list member for ({}, {})",user.email(),email());
}
return false;
}
public static List<MailingList> listActive() {
try {
var list = new ArrayList<MailingList>();
var rs = Database.open().select(TABLE_NAME).where(STATE+" % 2",1).compile().exec();
while (rs.next()) list.add(MailingList.from(rs));
return list;
} catch (SQLException e){
LOG.debug("Failed to load active MailingLists");
}
return List.of();
}
/**
* Load a ML object by it's identifying email address.
* This is a cached method: if the ML has been loaded before, the already-loaded object will be returned.
@ -326,12 +297,11 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -326,12 +297,11 @@ public class MailingList implements MessageHandler, ProblemListener {
ml.lastError = rs.getString(LAST_ERROR);
}
} catch (SQLException e) {
LOG.debug("Failed to load MailingList:",e);
LOG.debug("Failed to load MailingList: ",e);
}
return ml;
}
/**
* Load a ML object by it's identifying email address.
* This is a cached method: if the ML has been loaded before, the already-loaded object will be returned.
@ -349,13 +319,12 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -349,13 +319,12 @@ public class MailingList implements MessageHandler, ProblemListener {
.compile().exec();
while (rs.next()) list.add(MailingList.from(rs));
} catch (SQLException e) {
LOG.debug("Failed to load MailingLists:",e);
LOG.debug("Failed to load MailingLists: ",e);
}
return list;
}
public boolean mayBeAlteredBy(User user) {
if (user == null) return false;
if (user.hashPermission(PERMISSION_ADMIN)) return true;
try {
if (ListMember.load(this,user).isModerator()) return true;
@ -446,37 +415,44 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -446,37 +415,44 @@ public class MailingList implements MessageHandler, ProblemListener {
@Override
public void onMessageReceived(Message message) throws MessagingException {
LOG.info("Message received: {}",message.getFrom());
String subject = message.getSubject();
Address from = message.getFrom()[0];
LOG.info("{} received message: \"{}\" from {}",email ,subject, from);
try {
if (subject.toLowerCase().contains("undelivered")){
LOG.warn("This one only goes to the mods!");
forward(message,moderators());
return;
}
Address from = message.getFrom()[0];
if (from instanceof InternetAddress internetAddress) {
var senderEmail = internetAddress.getAddress();
var user = User.load(senderEmail);
if (user == null) { // no subscription
if (this.isOpenForGuests()) {
forward(message,subscribers());
} else {
retainMessage(message);
sentRetentionNotification(senderEmail);
}
return;
}
var member = ListMember.load(this, user);
if (member == null || member.isAwaiting()) { // no subscription
if (this.isOpenForGuests()) {
forward(message, subscribers());
} else if (this.isOpenForSubscribers()){
forward(message,subscribers());
} else {
retainMessage(message);
sentRetentionNotification(senderEmail);
} else {
// at this point, the message is from a non-member and the list
// is only open to moderators. → forward to moderators
forward(message, moderators());
}
return;
}
// at this point the member is at least a subscriber!
if (member.isModerator() || this.isOpenForSubscribers()) {
forward(message,subscribers());
} else {
@ -517,6 +493,10 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -517,6 +493,10 @@ public class MailingList implements MessageHandler, ProblemListener {
return list;
}
public Stream<ListMember> owners() throws SQLException {
return members().filter(ListMember::isOwner);
}
public MailingList replyToList(boolean on) throws SQLException {
return setFlag(STATE_REPLY_TO_LIST,on);
}
@ -583,7 +563,6 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -583,7 +563,6 @@ public class MailingList implements MessageHandler, ProblemListener {
.set(SMTP_USER, smtp.username())
.set(SMTP_PASS, smtp.password())
.set(STATE, state)
.set(HOLD_TIME,holdTime)
.compile()
.run();
return this;
@ -622,15 +601,15 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -622,15 +601,15 @@ public class MailingList implements MessageHandler, ProblemListener {
public Map<String,Integer> stateMap(){
var map = new HashMap<String,Integer>();
if (hasState(STATE_ENABLED)) map.put(t("enabled"),VISIBLE);
if (hasState(STATE_PUBLIC)) map.put(t("public"),VISIBLE);
if (hasState(STATE_FORWARD_FROM)) map.put(t("original_from"),HIDDEN);
if (hasState(STATE_FORWARD_ATTACHED)) map.put(t("forward_attached"),HIDDEN);
if (hasState(STATE_HIDE_RECEIVERS)) map.put(t("hide_receivers"),HIDDEN);
if (hasState(STATE_REPLY_TO_LIST)) map.put(t("reply_to_list"),HIDDEN);
if (isOpenForGuests()) map.put(t("open_for_guests"),HIDDEN);
if (isOpenForSubscribers()) map.put(t("open_for_subscribers"),HIDDEN);
if (hasPublicArchive()) map.put(t("archive"),VISIBLE);
if (hasState(STATE_ENABLED)) map.put("enabled",VISIBLE);
if (hasState(STATE_PUBLIC)) map.put("public",VISIBLE);
if (hasState(STATE_FORWARD_FROM)) map.put("original_from",HIDDEN);
if (hasState(STATE_FORWARD_ATTACHED)) map.put("forward_attached",HIDDEN);
if (hasState(STATE_HIDE_RECEIVERS)) map.put("hide_receivers",HIDDEN);
if (hasState(STATE_REPLY_TO_LIST)) map.put("reply_to_list",HIDDEN);
if (hasState(STATE_OPEN_FOR_GUESTS)) map.put("open_for_guests",HIDDEN);
if (hasState(STATE_OPEN_FOR_SUBSCRIBERS)) map.put("open_for_subscribers",HIDDEN);
if (hasState(STATE_PUBLIC_ARCHIVE)) map.put("archive",VISIBLE);
return map;
}
@ -686,24 +665,20 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -686,24 +665,20 @@ public class MailingList implements MessageHandler, ProblemListener {
* @throws MessagingException
*/
public void requestSubscription(User user, boolean skipConfirmation, ST template) throws SQLException, MessagingException {
var state = skipConfirmation ? STATE_SUBSCRIBER : STATE_AWAITING_CONFIRMATION;
var state = skipConfirmation ? ListMember.STATE_SUBSCRIBER : ListMember.STATE_AWAITING_CONFIRMATION;
var member = ListMember.create(this,user,state);
if (skipConfirmation) return;
try {
var config = Configuration.instance();
var url = new StringBuilder(config.baseUrl()).append("/web/confirm?token=").append(member.token()).toString();
var subject = t("[{}] Please confirm your list subscription!",name());
var subject = t("[{}] Please confirm your list subscription",name());
var text = template.add(URL,url).add(LIST_NAME,name()).render();
smtp.send(email(),name(),user.email(),subject,text);
} catch (UnsupportedEncodingException e) {
throw new MessagingException(t("Failed to send email to {}!",user.email()),e);
throw new MessagingException(t("Failed to send email to {}",user.email()),e);
}
}
public void sendPasswordReset(String email, String subject,String text) throws MessagingException, UnsupportedEncodingException {
smtp.send(email(),name(),email,subject,text);
}
protected SmtpClient smtp(){
return smtp;
}
@ -736,7 +711,7 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -736,7 +711,7 @@ public class MailingList implements MessageHandler, ProblemListener {
* @throws UnsupportedEncodingException
*/
public void test(User user) throws MessagingException, UnsupportedEncodingException {
var subject = t("[{}] test mail",name());
var subject = t("[{}]: test mail",name());
var text = "If you received this mail, the SMTP settings of your mailing list are correct.";
smtp.login().send(email(),name(),user.email(),subject,text);
}
@ -750,32 +725,6 @@ public class MailingList implements MessageHandler, ProblemListener { @@ -750,32 +725,6 @@ public class MailingList implements MessageHandler, ProblemListener {
}
}
public String transfer(User owner, String newOwnerMail) {
User newOwner;
try {
newOwner = User.load(newOwnerMail);
} catch (SQLException e) {
return t("Failed to load user for address {}",newOwnerMail);
}
ListMember member;
try {
member = ListMember.load(this,newOwner);
} catch (SQLException e) {
return t("{} is not a member of {}",newOwner);
}
try {
member.setState(STATE_OWNER);
} catch (SQLException e) {
return t("Failed to transfer list ownership to {}",newOwner);
}
try {
ListMember.load(this,owner).setState(STATE_MODERATOR);
} catch (SQLException e) {
return t("Failed to withdraw ownership from {}",owner);
}
return null;
}
public void update(String name, String email, String imapHost, Integer imapPort, String imapUser, String imapPass, String inbox, String smtpHost, Integer smtpPort, String smtpUser, String smtpPass) throws SQLException {
imap.stop();
Database.open()

52
src/main/java/de/srsoftware/widerhall/data/Post.java

@ -15,7 +15,9 @@ import java.nio.file.Files; @@ -15,7 +15,9 @@ import java.nio.file.Files;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Constants.VARCHAR;
@ -23,13 +25,12 @@ import static de.srsoftware.widerhall.Constants.VARCHAR; @@ -23,13 +25,12 @@ import static de.srsoftware.widerhall.Constants.VARCHAR;
public class Post {
public static final Logger LOG = LoggerFactory.getLogger(Post.class);
public static final String TABLE_NAME = "Posts";
private static final String DATE = "date";
private static final String FILE = "file";
private static final String FROM_ADDR = "from_addr";
private static final String FROM_NAME = "from_name";
private static final String LONG = "LONG";
private static final String PARENT = "parent";
private static final String LONG = "LONG";
private static final String DATE = "date";
private static final String FILE = "file";
private static HashMap<String, Post> cache = new HashMap<>();
private String id, fromAddr, fromName, subject, filename;
@ -64,7 +65,7 @@ public class Post { @@ -64,7 +65,7 @@ public class Post {
file.getParentFile().mkdirs();
Files.writeString(file.toPath(),text, StandardCharsets.UTF_8);
return post.save();
} catch (Exception e) {
} catch (MessagingException | IOException | SQLException e) {
LOG.warn("Failed to create post from {}",message);
}
return null;
@ -113,24 +114,6 @@ public class Post { @@ -113,24 +114,6 @@ public class Post {
}
}
public static ArrayList<Post> find(MailingList list, String month, List<String> allowedSenders) throws SQLException {
var query = Database.open()
.select(TABLE_NAME,"*","strftime('%Y-%m',date/1000,'unixepoch') as month")
.where(LIST,list.email())
.where(MONTH,month);
if (allowedSenders != null) query = query.where(FROM_ADDR,allowedSenders);
var rs = query.sort(DATE)
.compile()
.exec();
try {
var result = new ArrayList<Post>();
while (rs.next()) result.add(Post.from(rs));
return result;
} finally {
rs.close();
}
}
private static Post from(ResultSet rs) {
try {
var id = rs.getString(ID);
@ -157,10 +140,6 @@ public class Post { @@ -157,10 +140,6 @@ public class Post {
return id;
}
public MailingList list() {
return list;
}
public static Post load(String id) throws SQLException {
var rs = Database.open().select(TABLE_NAME).where(ID,id).compile().exec();
try {
@ -181,11 +160,6 @@ public class Post { @@ -181,11 +160,6 @@ public class Post {
FILE,filename);
}
public void remove() throws SQLException {
Database.open().deleteFrom(TABLE_NAME).where(ID,id).compile().run();
file().delete();
}
public Map<String,Object> safeMap() {
return Map.of(ID,id,
LIST,list.name(),
@ -199,13 +173,10 @@ public class Post { @@ -199,13 +173,10 @@ public class Post {
return this;
}
public static Map<String, Object> summarize(MailingList list,List<String> limitedUsers) throws SQLException {
var sql = new StringBuilder("SELECT count(*) as count,strftime('%Y-%m',date/1000,'unixepoch') as month FROM Posts");
var query = Database.open().query(sql).where(LIST,list.email()).groupBy(MONTH).sort(MONTH);
if (limitedUsers != null) query.where(FROM_ADDR,limitedUsers);
var rs = query.compile().exec();
var map = new TreeMap<String,Object>();
public static HashMap<String, Object> summarize(MailingList list) throws SQLException {
var sql = new StringBuilder("SELECT count(*) as count,strftime('%Y-%m',date/1000,'unixepoch') as month FROM Posts WHERE list = ? GROUP BY month ORDER BY month;");
var map = new HashMap<String,Object>();
var rs = Database.open().query(sql).compile(list.email()).exec();
while (rs.next()) map.put(rs.getString("month"),rs.getInt("count"));
rs.close();
return map;
@ -214,4 +185,5 @@ public class Post { @@ -214,4 +185,5 @@ public class Post {
public long timestamp(){
return timestamp;
}
}

61
src/main/java/de/srsoftware/widerhall/data/User.java

@ -6,11 +6,9 @@ import java.security.InvalidKeyException; @@ -6,11 +6,9 @@ import java.security.InvalidKeyException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Util.t;
/**
* @author Stephan Richter
@ -21,11 +19,10 @@ public class User { @@ -21,11 +19,10 @@ public class User {
public static final int PERMISSION_ADMIN = 1;
public static final int PERMISSION_CREATE_LISTS = 2;
public static final String HASHED_PASS = "hashedPassword";
public static final String RESET_TOKEN = "resetToken";
public static final String SALT = "salt";
private static final HashMap<String,User> users = new HashMap<>();
private String email, salt, hashedPass, name, token;
private String email, salt, hashedPass, name;
private int permissions;
/**
@ -36,12 +33,11 @@ public class User { @@ -36,12 +33,11 @@ public class User {
* @param hashedPass
* @param permissions
*/
public User(String email, String name, String salt, String hashedPass, String token, int permissions) {
public User(String email, String name, String salt, String hashedPass, int permissions) {
this.email = email.toLowerCase();
this.name = name;
this.salt = salt;
this.hashedPass = hashedPass;
this.token = token;
this.permissions = permissions;
}
@ -84,21 +80,6 @@ public class User { @@ -84,21 +80,6 @@ public class User {
.run();
}
public static void addTokenColumn() throws SQLException {
String sql = "ALTER TABLE %s ADD COLUMN %s %s;".formatted(TABLE_NAME,RESET_TOKEN,VARCHAR);
Database.open().query(sql).compile().run();
}
public static User byToken(String token) throws SQLException {
if (token == null || token.isBlank()) return null;
var rs = Database.open().select(TABLE_NAME).where(RESET_TOKEN,token).compile().exec();
try {
if (rs.next()) return User.from(rs);
return null;
} finally {
rs.close();
}
}
/**
* Create a new user object by hashing it's password and storing user data, salt and hashed password to the db.
@ -114,10 +95,10 @@ public class User { @@ -114,10 +95,10 @@ public class User {
String salt = null;
String hashedPass = null;
if (password != null) {
salt = Util.sha256(email + LocalDateTime.now() + name);
salt = Util.sha256(email + name + LocalDate.now());
hashedPass = Util.sha256(password + salt);
}
return new User(email,name,salt,hashedPass,null,0).save();
return new User(email,name,salt,hashedPass,0).save();
}
/**
@ -133,15 +114,11 @@ public class User { @@ -133,15 +114,11 @@ public class User {
.append(PERMISSIONS).append(" ").append(INT).append(", ")
.append(SALT).append(" ").append(VARCHAR).append(", ")
.append(HASHED_PASS).append(" ").append(VARCHAR)
.append(");");
Database.open().query(sql).compile().run();
}
public void dropPasswordToken() throws SQLException {
Database.open().update(TABLE_NAME).set(RESET_TOKEN,null).where(EMAIL,email).compile().run();
}
/**
* Withdraw a specific permission from the user object.
* Updated permission flag will be written to db.
@ -198,7 +175,6 @@ public class User { @@ -198,7 +175,6 @@ public class User {
if (emails != null) query.where(EMAIL,emails);
var rs = query.compile().exec();
while (rs.next()) userList.add(User.from(rs));
Collections.sort(userList,(u1,u2)->u1.name.compareTo(u2.name));
return userList;
}
@ -217,16 +193,10 @@ public class User { @@ -217,16 +193,10 @@ public class User {
rs.getString(NAME),
rs.getString(SALT),
rs.getString(HASHED_PASS),
rs.getString(RESET_TOKEN),
rs.getInt(PERMISSIONS)));
return user;
}
public String generateToken() throws SQLException {
token = Util.randomString(64);
Database.open().update(TABLE_NAME).set(RESET_TOKEN,token).where(EMAIL,this.email).compile().run();
return token;
}
/**
* Loads the user identified by it's email, but only if the provided password matches.
@ -297,7 +267,7 @@ public class User { @@ -297,7 +267,7 @@ public class User {
* @return
*/
public Map<String,String> safeMap(){
return Map.of(NAME,name,EMAIL,email,PERMISSIONS,permissionList(),PASSWORD,t(hashedPassword() == null ? "no" : "yes"));
return Map.of(NAME,name,EMAIL,email,PERMISSIONS,permissionList(),PASSWORD,hashedPassword() == null ? "no" : "yes");
}
/**
@ -312,23 +282,4 @@ public class User { @@ -312,23 +282,4 @@ public class User {
req.compile().run();
return this;
}
public void setPassword(String newPassword) throws SQLException {
if (newPassword != null) {
String newSalt = Util.sha256(email + LocalDateTime.now() + name);
String newHashedPass = Util.sha256(newPassword + newSalt);
Database.open().update(TABLE_NAME).set(HASHED_PASS,newHashedPass).set(SALT,newSalt).where(EMAIL,email).compile().run();
hashedPass = newHashedPass;
salt = newSalt;
}
}
public String token() {
return token;
}
@Override
public String toString() {
return name == null ? email : name;
}
}

125
src/main/java/de/srsoftware/widerhall/mail/ImapClient.java

@ -33,40 +33,28 @@ public class ImapClient { @@ -33,40 +33,28 @@ public class ImapClient {
return this;
}
public ListeningThread doStop() {
stopped = true;
return this;
}
public ListeningThread dropListeners() {
listeners.clear();
return this;
}
private void handle(Message message) throws MessagingException {
LOG.debug("Handling {}",message.getSubject());
for (MessageHandler listener : listeners) listener.onMessageReceived(message);
}
private void handleMessages() throws MessagingException {
LOG.debug("Reading email of {}:",username);
if (!inbox.isOpen()) inbox.open(IMAPFolder.READ_WRITE);
for (Message message : inbox.getMessages()){
if (message.isSet(Flags.Flag.SEEN)) continue;
handle(message);
Folder folder = message.getFolder();
if (!folder.isOpen()) folder.open(Folder.READ_WRITE);
message.setFlag(Flags.Flag.SEEN,true);
@Override
public void run() {
while (!stopped) {
try {
sleep(5000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
try {
openInbox();
} catch (MessagingException e){
LOG.warn("Connection problem:",e);
problemListener.onImapException(e);
}
}
}
private Properties imapProps() {
Properties props = new Properties();
props.put(Constants.PROTOCOL,Constants.IMAPS);
return props;
}
private void openInbox() throws MessagingException {
LOG.debug("Connecting and logging in…");
Properties props = imapProps();
@ -85,32 +73,39 @@ public class ImapClient { @@ -85,32 +73,39 @@ public class ImapClient {
}
}
@Override
public void run() {
while (!stopped) {
try {
sleep(5000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
try {
openInbox();
} catch (MessagingException e){
LOG.warn("Connection problem:",e);
problemListener.onImapException(e);
}
private void handleMessages() throws MessagingException {
LOG.debug("Reading email of {}:",username);
if (!inbox.isOpen()) inbox.open(IMAPFolder.READ_WRITE);
for (Message message : inbox.getMessages()){
if (message.isSet(Flags.Flag.SEEN)) continue;
handle(message);
Folder folder = message.getFolder();
if (!folder.isOpen()) folder.open(Folder.READ_WRITE);
message.setFlag(Flags.Flag.SEEN,true);
}
}
}
private class Heartbeat extends Thread{
private static final Logger LOG = LoggerFactory.getLogger(Heartbeat.class);
private boolean stopped = false;
private void handle(Message message) throws MessagingException {
LOG.debug("Handling {}",message.getSubject());
for (MessageHandler listener : listeners) listener.onMessageReceived(message);
}
public Heartbeat doStop() {
private Properties imapProps() {
Properties props = new Properties();
props.put(Constants.PROTOCOL,Constants.IMAPS);
return props;
}
public ListeningThread doStop() {
stopped = true;
return this;
}
}
private class Heartbeat extends Thread{
private static final Logger LOG = LoggerFactory.getLogger(Heartbeat.class);
private boolean stopped = false;
@Override
public void run() {
@ -128,6 +123,11 @@ public class ImapClient { @@ -128,6 +123,11 @@ public class ImapClient {
}
}
}
public Heartbeat doStop() {
stopped = true;
return this;
}
}
public ImapClient(String host, int port, String username, String password, String folderName,ProblemListener listener) {
@ -154,9 +154,8 @@ public class ImapClient { @@ -154,9 +154,8 @@ public class ImapClient {
Date receivedDate = message.getReceivedDate();
Duration duration = Duration.between(receivedDate.toInstant(),now.toInstant());
var days = duration.toDays();
LOG.info("Message {} is {} days old!",message.getSubject(),days);
LOG.info("Message {} is {} days old!");
if (days > holdTime){
LOG.info("…removing");
Folder folder = message.getFolder();
if (!folder.isOpen()) folder.open(Folder.READ_WRITE);
message.setFlag(Flags.Flag.DELETED, true);
@ -165,15 +164,27 @@ public class ImapClient { @@ -165,15 +164,27 @@ public class ImapClient {
inbox.expunge();
}
public String folderName(){
return folderName;
}
public String host(){
return host;
}
public String username(){
return username;
}
public String password(){
return password;
}
public int port(){
return port;
}
public String folderName(){
return folderName;
}
public ImapClient move(Message message, String destinationFolder) throws MessagingException {
if (listeningThread == null || listeningThread.stopped) throw new IllegalStateException("IMAP client not connected!");
var source = message.getFolder();
@ -188,14 +199,6 @@ public class ImapClient { @@ -188,14 +199,6 @@ public class ImapClient {
return this;
}
public String password(){
return password;
}
public int port(){
return port;
}
public ImapClient start() {
stop();
@ -221,8 +224,4 @@ public class ImapClient { @@ -221,8 +224,4 @@ public class ImapClient {
}
return this;
}
public String username(){
return username;
}
}

3
src/main/java/de/srsoftware/widerhall/mail/ProblemListener.java

@ -3,6 +3,7 @@ package de.srsoftware.widerhall.mail; @@ -3,6 +3,7 @@ package de.srsoftware.widerhall.mail;
import javax.mail.MessagingException;
public interface ProblemListener {
public void clearProblems();
public void onImapException(MessagingException e);
public void clearProblems();
}

45
src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java

@ -11,8 +11,6 @@ import java.util.Date; @@ -11,8 +11,6 @@ import java.util.Date;
import java.util.List;
import java.util.Properties;
import static de.srsoftware.widerhall.Util.t;
public class SmtpClient {
private static final Logger LOG = LoggerFactory.getLogger(SmtpClient.class);
private static final String HOST = "mail.smtp.host";
@ -52,10 +50,10 @@ public class SmtpClient { @@ -52,10 +50,10 @@ public class SmtpClient {
}
if (newSender != null){
forward.setFrom(newSender);
forward.setSubject(subject+" (from "+oldSender+")","utf-8");
forward.setSubject(subject+" (from "+oldSender+")");
} else {
forward.setFrom(oldSender);
forward.setSubject(subject,"utf-8");
forward.setSubject(subject);
}
if (replyTo != null) forward.setReplyTo(InternetAddress.parse(replyTo));
var recipientType = bcc ? Message.RecipientType.BCC : Message.RecipientType.TO;
@ -64,7 +62,7 @@ public class SmtpClient { @@ -64,7 +62,7 @@ public class SmtpClient {
MimeMultipart multipart = new MimeMultipart();
if (forwardAsAttachment){
MimeBodyPart bodyPart = new MimeBodyPart();
bodyPart.setText(t("Find the forwarded message in the attachment(s)!\n"));
bodyPart.setText("Find the forwarded message in the attachment(s)!\n");
multipart.addBodyPart(bodyPart);
// create another body part to contain the message to be forwarded
@ -83,9 +81,6 @@ public class SmtpClient { @@ -83,9 +81,6 @@ public class SmtpClient {
send(forward);
}
public String host() {
return host;
}
public SmtpClient login(){
if (session == null) {
@ -102,20 +97,6 @@ public class SmtpClient { @@ -102,20 +97,6 @@ public class SmtpClient {
return this;
}
public String password() {
return password;
}
public int port() {
return port;
}
public void send(Message message) throws MessagingException {
LOG.debug("sending mail…");
Transport.send(message,username,password);
LOG.debug("…sent");
}
public void send(String senderAdress, String senderName, String receivers, String subject, String content) throws MessagingException, UnsupportedEncodingException {
login();
MimeMessage message = new MimeMessage(session);
@ -132,7 +113,27 @@ public class SmtpClient { @@ -132,7 +113,27 @@ public class SmtpClient {
send(message);
}
public void send(Message message) throws MessagingException {
LOG.debug("Versende Mail…");
Transport.send(message,username,password);
LOG.debug("…versendet");
}
public String host() {
return host;
}
public int port() {
return port;
}
public String username() {
return username;
}
public String password() {
return password;
}
}

125
src/main/java/de/srsoftware/widerhall/web/Rest.java

@ -18,7 +18,6 @@ import java.sql.SQLException; @@ -18,7 +18,6 @@ import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Util.t;
@ -40,8 +39,6 @@ public class Rest extends HttpServlet { @@ -40,8 +39,6 @@ public class Rest extends HttpServlet {
private static final String LIST_SHOW = "list/show";
private static final String LIST_TEST = "list/test";
private static final String LIST_SUBSCRIBABLE = "list/subscribable";
private static final String MAIL_DROP = "mail/drop";
private static final String TRANSFER = "list/transfer";
private static final String USER_ADD_PERMISSION = "user/addpermission";
private static final String USER_DROP_PERMISSION = "user/droppermission";
private static final String USER_LIST = "user/list";
@ -49,44 +46,42 @@ public class Rest extends HttpServlet { @@ -49,44 +46,42 @@ public class Rest extends HttpServlet {
private static final String SUCCESS = "success";
private Map addPermission(String userEmail, String permissions) {
if (userEmail == null || userEmail.isBlank()) return Map.of(ERROR,t("missing user email address!"));
if (userEmail == null || userEmail.isBlank()) return Map.of(ERROR,"missing user email address!");
try {
int perm = Integer.parseInt(permissions);
var user = User.loadAll(List.of(userEmail)).stream().findAny().orElse(null);
if (user == null) return Map.of(ERROR,t("Failed to load user for address {}!",userEmail));
if (user == null) return Map.of(ERROR,t("Failed to load user for address {}",userEmail));
user.addPermission(perm);
} catch (NumberFormatException nfe){
return Map.of(ERROR,"no valid permissions provided!");
} catch (SQLException e) {
LOG.debug("Failed to load user for address {}!",userEmail,e);
return Map.of(ERROR,t("Failed to load user for address {}!",userEmail));
LOG.debug("Failed to load user for address {}",userEmail,e);
return Map.of(ERROR,t("Failed to load user for address {}",userEmail));
}
return Map.of(SUCCESS,"Updated user permissions");
}
private Map<String,Object> archive(HttpServletRequest req, User user) throws SQLException {
private Map<String, Object> archive(HttpServletRequest req) {
var list = Util.getMailingList(req);
if (list == null) throw new IllegalArgumentException(t("You are trying to access a non-existing list!"));
var allowed = list.hasPublicArchive() || list.mayBeAlteredBy(user);
if (!allowed) throw new IllegalAccessError(t("You are not allowed to access the archive of this list!"));
var allEmails = user != null || list.hasState(STATE_OPEN_FOR_SUBSCRIBERS) || list.hasState(STATE_OPEN_FOR_GUESTS);
var limitedSenders = allEmails ? null : list.moderators().map(ListMember::user).map(User::email).toList();
boolean userIsMod = list.mayBeAlteredBy(user);
String month = req.getParameter(MONTH);
if (month == null || month.isBlank()) return Map.of(LIST,list.email(),MODERATOR,userIsMod,"summary",Post.summarize(list,limitedSenders));
return Map.of(LIST,list.email(),MODERATOR,userIsMod,"posts",Post.find(list,month,limitedSenders).stream().map(Post::safeMap).toList());
if (list != null){
try {
var month = req.getParameter(MONTH);
if (month == null || month.isBlank()) {
return Map.of(LIST,list.email(),"summary",Post.summarize(list));
} else {
return Map.of(LIST,list.email(),"posts",Post.find(list,month).stream().map(Post::safeMap).toList());
}
} catch (SQLException e) {
e.printStackTrace();
}
}
LOG.debug("list: {}",list.email());
return Map.of();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String error;
try {
error = handleGet(req, resp);
} catch (SQLException e) {
error = e.getMessage();
}
String error = handleGet(req, resp);
if (error != null) resp.sendError(400,error);
}
@ -123,35 +118,20 @@ public class Rest extends HttpServlet { @@ -123,35 +118,20 @@ public class Rest extends HttpServlet {
if (error != null) resp.sendError(400,error);
}
private Map dropMail(String messageId,User user){
try {
var message = Post.load(messageId);
if (message == null) return Map.of(ERROR,t("Cannot remove: unknown message id"));
var allowed = message.list().mayBeAlteredBy(user);
if (allowed){
message.remove();
return Map.of(SUCCESS,t("Message deleted"));
}
return Map.of(ERROR,t("You are not allowed to remove messages from this list!"));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Map dropPermission(String userEmail, String permissions) {
if (userEmail == null || userEmail.isBlank()) return Map.of(ERROR,"missing user email address!");
try {
int perm = Integer.parseInt(permissions);
var user = User.loadAll(List.of(userEmail)).stream().findAny().orElse(null);
if (user == null) return Map.of(ERROR,t("Failed to load user for address {}!",userEmail));
if (user == null) return Map.of(ERROR,t("Failed to load user for address {}",userEmail));
user.dropPermission(perm);
} catch (NumberFormatException nfe){
return Map.of(ERROR,"no valid permissions provided!");
} catch (SQLException e) {
LOG.debug("Failed to load user for address {}!",userEmail,e);
return Map.of(ERROR,t("Failed to load user for address {}!",userEmail));
LOG.debug("Failed to load user for address {}",userEmail,e);
return Map.of(ERROR,t("Failed to load user for address {}",userEmail));
}
return Map.of(SUCCESS,t("Updated user permissions"));
return Map.of(SUCCESS,"Updated user permissions");
}
private Map enableList(MailingList list, User user, boolean enable) {
@ -159,14 +139,14 @@ public class Rest extends HttpServlet { @@ -159,14 +139,14 @@ public class Rest extends HttpServlet {
if (!list.mayBeAlteredBy(user)) Map.of(ERROR,t("You are not allowed to edit '{}'",list.email()));
try {
list.enable(enable);
return Map.of(SUCCESS,t("Mailing list '{}' was {}!",list.email(),t(enable ? "enabled" : "disabled")));
return Map.of(SUCCESS,t("Mailing list '{}' was {}!",list.email(),enable ? "enabled" : "disabled"));
} catch (SQLException e) {
LOG.error("Failed to enable/disable mailing list:",e);
return Map.of(ERROR,t("Failed to update list '{}'!",list.email()));
LOG.error("Failed to enable/disable mailing list: ",e);
return Map.of(ERROR,t("Failed to update list '{}'",list.email()));
}
}
public String handleGet(HttpServletRequest req, HttpServletResponse resp) throws SQLException {
public String handleGet(HttpServletRequest req, HttpServletResponse resp){
var user = Util.getUser(req);
var path = Util.getPath(req);
@ -176,18 +156,18 @@ public class Rest extends HttpServlet { @@ -176,18 +156,18 @@ public class Rest extends HttpServlet {
json.put(USER,user.safeMap());
switch (path) {
case LIST_ARCHIVE:
json.put("archive",archive(req,user));
json.put("archive",archive(req));
break;
case USER_LIST:
try {
json.put("users", (user.hashPermission(User.PERMISSION_ADMIN) ? User.loadAll() : List.of(user)).stream().map(User::safeMap).toList());
} catch (SQLException e) {
LOG.debug("Failed to load user list:",e);
json.put(ERROR,t("failed to load user list"));
json.put(ERROR,"failed to load user list");
}
break;
case LIST_MODERATED:
json.put("lists", MailingList.moderatedBy(user).stream().sorted((l1,l2)->l1.name().compareTo(l2.name())).map(MailingList::safeMap).toList());
json.put("lists", MailingList.moderatedBy(user).stream().map(MailingList::safeMap).toList());
break;
case LIST_SUBSCRIBABLE:
json.put("lists", MailingList.subscribable(user).stream().map(MailingList::minimalMap).toList());
@ -199,13 +179,13 @@ public class Rest extends HttpServlet { @@ -199,13 +179,13 @@ public class Rest extends HttpServlet {
} else {
switch (path) {
case LIST_ARCHIVE:
json.put("archive",archive(req,null));
json.put("archive",archive(req));
break;
case LIST_SUBSCRIBABLE:
json.put("lists", MailingList.subscribable().stream().map(MailingList::minimalMap).toList());
break;
default:
json.put(ERROR,t("Not logged in!"));
json.put(ERROR,"Not logged in!");
}
}
try {
@ -265,35 +245,25 @@ public class Rest extends HttpServlet { @@ -265,35 +245,25 @@ public class Rest extends HttpServlet {
case LIST_TEST:
json.putAll(testList(list,user));
break;
case MAIL_DROP:
var messageId = req.getParameter(MESSAGE_ID);
json.putAll(dropMail(messageId,user));
break;
case TRANSFER:
if (list.isOwnedBy(user)){
Optional.ofNullable(list.transfer(user,userEmail)).ifPresent(err -> json.put(ERROR,err));
} else json.put(ERROR,t("As you don't own this list, you may not transfer its ownership!"));
break;
case USER_ADD_PERMISSION:
if (user.hashPermission(User.PERMISSION_ADMIN)){
json.putAll(addPermission(userEmail,permissions));
} else json.put(ERROR,t("You are not allowed to alter user permissions!"));
} else json.put(ERROR,"You are not allowed to alter user permissions!");
break;
case USER_DROP_PERMISSION:
if (user.hashPermission(User.PERMISSION_ADMIN)){
json.putAll(dropPermission(userEmail,permissions));
} else json.put(ERROR,t("You are not allowed to alter user permissions!"));
} else json.put(ERROR,"You are not allowed to alter user permissions!");
break;
default:
json.put(ERROR,t("No handler for path '{}'!",path));
break;
}
} else {
json.put(ERROR,t("Not logged in!"));
json.put(ERROR,"Not logged in!");
}
try {
resp.setContentType("application/json");
if (json.containsKey(ERROR)) resp.setStatus(404);
resp.getWriter().println(json.toJSONString());
return null;
} catch (IOException e) {
@ -306,9 +276,9 @@ public class Rest extends HttpServlet { @@ -306,9 +276,9 @@ public class Rest extends HttpServlet {
if (!list.mayBeAlteredBy(user)) Map.of(ERROR,t("You are not allowed to edit '{}'",list.email()));
try {
list.hide(hide);
return Map.of(SUCCESS,t("Mailing list '{}' was {}!",list.email(),t(hide ? "hidden" : "made public")));
return Map.of(SUCCESS,t("Mailing list '{}' was {}!",list.email(),hide ? "hidden" : "made public"));
} catch (SQLException e) {
LOG.error("Failed to (un)hide mailing list:",e);
LOG.error("Failed to (un)hide mailing list: ",e);
return Map.of(ERROR,t("Failed to update list '{}'",list.email()));
}
}
@ -321,7 +291,7 @@ public class Rest extends HttpServlet { @@ -321,7 +291,7 @@ public class Rest extends HttpServlet {
LOG.warn("Failed to load list member for {}/{}",user.email(),list.email(),e);
return Map.of(ERROR,t("Failed to load list member for {}/{}",user.email(),list.email()));
}
if (moderator == null) return Map.of(ERROR,t("{} is not a member of {}!",user.email(),list.email()));
if (moderator == null) return Map.of(ERROR,t("{} is not a member of {}",user.email(),list.email()));
var error = moderator.addNewModerator(userEmail);
@ -329,7 +299,7 @@ public class Rest extends HttpServlet { @@ -329,7 +299,7 @@ public class Rest extends HttpServlet {
}
private Map listDetail(MailingList list, User user) {
if (list == null) return Map.of(ERROR,t("no list email provided!"));
if (list == null) return Map.of(ERROR,"no list email provided!");
var map = new HashMap<>();
if (list.hasState(MailingList.STATE_FORWARD_FROM)) map.put(KEY_FORWARD_FROM,true);
if (list.hasState(MailingList.STATE_FORWARD_ATTACHED)) map.put(KEY_FORWARD_ATTACHED,true);
@ -337,7 +307,7 @@ public class Rest extends HttpServlet { @@ -337,7 +307,7 @@ public class Rest extends HttpServlet {
if (list.hasState(MailingList.STATE_REPLY_TO_LIST)) map.put(KEY_REPLY_TO_LIST,true);
if (list.isOpenForGuests()) map.put(KEY_OPEN_FOR_GUESTS,true);
if (list.isOpenForSubscribers()) map.put(KEY_OPEN_FOR_SUBSCRIBERS,true);
if (list.hasPublicArchive()) map.put(KEY_ARCHIVE,true);
if (list.hasState(MailingList.STATE_PUBLIC_ARCHIVE)) map.put(KEY_ARCHIVE,true);
if (list.hasState(STATE_MODS_CAN_EDIT_MODS)) map.put(KEY_MODS_CAN_EDIT_MODS,true);
if (list.holdTime() != null) map.put(KEY_DELETE_MESSAGES,list.holdTime());
return map;
@ -374,29 +344,28 @@ public class Rest extends HttpServlet { @@ -374,29 +344,28 @@ public class Rest extends HttpServlet {
}
private Map<String, Object> listMembers(MailingList list, User user) {
if (list == null) return Map.of(ERROR,t("no list email provided!"));
if (!list.membersMayBeListedBy(user)) Map.of(ERROR,t("You are not allowed to list members of '{}'!",list.email()));
if (list == null) return Map.of(ERROR,"no list email provided!");
if (!list.membersMayBeListedBy(user)) Map.of(ERROR,t("You are not allowed to list members of '{}'",list.email()));
try {
var members = list.members()
.sorted((m1,m2)->m1.user().name().compareTo(m2.user().name()))
.map(ListMember::safeMap)
.toList();
return Map.of(MEMBERS,members,LIST,list.minimalMap());
} catch (SQLException e) {
LOG.error("Failed to load member list: ",e);
return Map.of(ERROR,t("Failed to load member list '{}'!",list.email()));
return Map.of(ERROR,t("Failed to load member list '{}'",list.email()));
}
}
private Map testList(MailingList list, User user) {
if (list == null) return Map.of(ERROR,t("no list email provided!"));
if (!list.mayBeTestedBy(user)) Map.of(ERROR,t("You are not allowed to test '{}'!",list.email()));
if (list == null) return Map.of(ERROR,"no list email provided!");
if (!list.mayBeTestedBy(user)) Map.of(ERROR,t("You are not allowed to test '{}'",list.email()));
try {
list.test(user);
return Map.of(SUCCESS,t("Sent test email to {}",user.email()));
} catch (Exception e) {
LOG.warn("Failed to send test email",e);
return Map.of(ERROR,t("Failed to send test email to {}!",user.email()));
return Map.of(ERROR,t("Failed to send test email to {}",user.email()));
}
}
}

2
src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java

@ -52,7 +52,7 @@ public abstract class TemplateServlet extends HttpServlet { @@ -52,7 +52,7 @@ public abstract class TemplateServlet extends HttpServlet {
resp.getWriter().println(template.render());
return null;
} catch (IOException e) {
return t("Failed to load template '{}'!",path);
return t("Failed to load template '{}'",path);
}
}
return t("No template for path '{}'!",path);

277
src/main/java/de/srsoftware/widerhall/web/Web.java

@ -15,7 +15,8 @@ import java.io.IOException; @@ -15,7 +15,8 @@ import java.io.IOException;
import java.nio.file.Files;
import java.security.InvalidKeyException;
import java.sql.SQLException;
import java.util.*;
import java.util.HashMap;
import java.util.Map;
import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Util.t;
@ -36,7 +37,6 @@ public class Web extends TemplateServlet { @@ -36,7 +37,6 @@ public class Web extends TemplateServlet {
private static final String POST = "post";
private static final String REGISTER = "register";
private static final String RELOAD = "reload";
private static final String RESET_PASSWORD = "reset-pw";
private static final String SUBSCRIBE = "subscribe";
private static final String UNSUBSCRIBE = "unsubscribe";
private static final String IMAP_HOST = "imap_host";
@ -49,7 +49,7 @@ public class Web extends TemplateServlet { @@ -49,7 +49,7 @@ public class Web extends TemplateServlet {
private static final String SMTP_PASS = "smtp_pass";
private static final int PRIMARY_KEY_CONSTRAINT = 19;
private String addList(HttpServletRequest req, HttpServletResponse resp, boolean isGet) {
private String addList(HttpServletRequest req, HttpServletResponse resp) {
var user = Util.getUser(req);
if (user == null) return redirectTo(LOGIN,resp);
var data = new HashMap<String, Object>();
@ -125,7 +125,7 @@ public class Web extends TemplateServlet { @@ -125,7 +125,7 @@ public class Web extends TemplateServlet {
}
try {
var list = MailingList.create(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, null);
var list = MailingList.create(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass);
ListMember.create(list, user, ListMember.STATE_OWNER);
return redirectTo(ADMIN, resp);
} catch (SQLException e) {
@ -133,22 +133,14 @@ public class Web extends TemplateServlet { @@ -133,22 +133,14 @@ public class Web extends TemplateServlet {
}
}
private String archive(MailingList list, User user, HttpServletRequest req, HttpServletResponse resp) {
if (list == null) return t("The mailing list you are trying to view does not exist!");
var allowed = list.hasPublicArchive() || list.mayBeAlteredBy(user);
if (!allowed) return t("You are not allowed to access the archive of this list");
var data = new HashMap<String,Object>();
if (user != null) data.put(USER,user);
data.put(LIST,list.email());
private String archive(HttpServletRequest req, HttpServletResponse resp) {
var map = new HashMap<String,Object>();
var list = Util.getMailingList(req);
map.put(LIST,list.email());
var month = req.getParameter(MONTH);
if (month != null && !month.isBlank()){
data.put(MONTH,month);
data.put(MODERATOR,list.mayBeAlteredBy(user));
}
return loadTemplate(ARCHIVE,data,resp);
if (month != null && !month.isBlank())map.put(MONTH,month);
return loadTemplate(ARCHIVE,map,resp);
}
private String confirm(HttpServletRequest req, HttpServletResponse resp) {
@ -161,7 +153,7 @@ public class Web extends TemplateServlet { @@ -161,7 +153,7 @@ public class Web extends TemplateServlet {
return loadTemplate(INDEX,Map.of(USER,listMember.user().safeMap(),NOTES,"Confirmed list subscription!"),resp);
}
return t("Unknown user or token!");
return t("Unknown user or token");
} catch (Exception e) {
LOG.debug("Failed to confirm list membership:",e);
return t("Confirmation of list membership failed!");
@ -170,17 +162,17 @@ public class Web extends TemplateServlet { @@ -170,17 +162,17 @@ public class Web extends TemplateServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String error = handleRequest(req, resp, true);
String error = handleGet(req, resp);
if (error != null) resp.sendError(400,error);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String error = handleRequest(req, resp, false);
String error = handlePost(req, resp);
if (error != null) resp.sendError(400,error);
}
public String editList(HttpServletRequest req, HttpServletResponse resp, boolean isGet) {
public String editList(HttpServletRequest req, HttpServletResponse resp) {
var user = Util.getUser(req);
if (user == null) return redirectTo(LOGIN,resp);
@ -189,7 +181,6 @@ public class Web extends TemplateServlet { @@ -189,7 +181,6 @@ public class Web extends TemplateServlet {
var list = Util.getMailingList(req);
data.put(LIST,list.safeMap());
if (isGet) return loadTemplate(EDIT_LIST,data,resp);
try {
var allowed = user.hashPermission(PERMISSION_ADMIN) || ListMember.load(list,user).isOwner();
if (!allowed) return loadTemplate(ADMIN,data,resp);
@ -262,7 +253,7 @@ public class Web extends TemplateServlet { @@ -262,7 +253,7 @@ public class Web extends TemplateServlet {
return loadTemplate(ADMIN,data,resp);
} catch (SQLException e) {
LOG.warn("Editing list {} by {} failed",list.email(),user.email(),e);
return t("Editing list {} by {} failed!",list.email(),user.email());
return t("Editing list {} by {} failed",list.email(),user.email());
}
}
@ -275,56 +266,11 @@ public class Web extends TemplateServlet { @@ -275,56 +266,11 @@ public class Web extends TemplateServlet {
return sqle;
}
private String handleLogin(HttpServletRequest req, HttpServletResponse resp,boolean isGet) {
if (isGet) return loadTemplate(LOGIN,null,resp);
var email = req.getParameter("email");
var pass = req.getParameter("pass");
if (email == null || pass == null) return loadTemplate(LOGIN, Map.of("error",t("Missing username or password!")), resp);
if (!Util.isEmail(email)) return loadTemplate(LOGIN, Map.of("error",t("'{}' is not a valid email address!",email)), resp);
try {
var user = User.loadUser(email,pass);
req.getSession().setAttribute("user",user);
// loading user successfull: goto index
resp.sendRedirect(String.join("/",WEB_ROOT,"admin"));
} catch (Exception e) {
try {
LOG.warn("Static.handleLogin failed:",e);
Thread.sleep(10000);
} finally {
return loadTemplate("login", Map.of(ERROR,t("Invalid username/password"),EMAIL,email), resp);
}
}
return null;
private User getSessionUser(HttpServletRequest req) {
return req.getSession().getAttribute(USER) instanceof User user ? user : null;
}
private String handleNewPassword(HttpServletRequest req, HttpServletResponse resp, boolean isGet) {
var user = Util.getUser(req);
if (user == null) return redirectTo(LOGIN,resp);
var data = new HashMap<String,Object>();
data.put(USER,user.safeMap());
if (!isGet) {
var pass = req.getParameter(PASSWORD);
var repeat = req.getParameter(PASSWORD_REPEAT);
if (pass == null || pass.isBlank()) {
data.put(ERROR, "Please set a password!");
} else if (!pass.equals(repeat)) {
data.put(ERROR, "Passwords do not match");
} else if (Util.simplePassword(pass)) {
data.put(ERROR, "Your password is to simple");
} else {
try {
user.setPassword(pass);
data.put(NOTES,"Your password has been updated!");
return loadTemplate(ADMIN,data,resp);
} catch (SQLException e) {
data.put(ERROR,t("Failed to update password in database: {}",e.getMessage()));
}
}
}
return loadTemplate(NEW_PASSWORD_FORM,data,resp);
}
private String handleRequest(HttpServletRequest req, HttpServletResponse resp, boolean isGet){
private String handleGet(HttpServletRequest req, HttpServletResponse resp) {
var path = Util.getPath(req);
var user = Util.getUser(req);
var data = new HashMap<String,Object>();
@ -332,78 +278,40 @@ public class Web extends TemplateServlet { @@ -332,78 +278,40 @@ public class Web extends TemplateServlet {
if (user != null) data.put(USER,user.safeMap());
if (list != null) data.put(LIST,list.minimalMap());
/*var interesting = !Set.of("js","jquery","css","OpenSans-Regular.woff","Bhineka.ttf").contains(path);
LOG.debug("{}",interesting?"interesting":"boring");*/
if (user != null){
switch (path){
case ADD_LIST:
return addList(req,resp,isGet);
case ADMIN:
return loadTemplate(path,data,resp);
case EDIT_LIST:
return editList(req,resp,isGet);
case INSPECT:
return inspect(req,resp,isGet);
case NEW_PASSWORD_FORM:
return handleNewPassword(req,resp,isGet);
}
}
String notes = null;
switch (path){
case ARCHIVE:
return archive(list,user,req,resp);
case CSS:
case INDEX:
return loadTemplate(path,data,resp);
return archive(req,resp);
case CONFIRM:
return confirm(req,resp);
case LOGIN:
return handleLogin(req,resp,isGet);
case LOGOUT:
req.getSession().invalidate();
return redirectTo(INDEX,resp);
case POST:
return post(req,resp);
case REGISTER:
return registerUser(req,resp,isGet);
case RELOAD:
loadTemplates();
data.put(NOTES,t("Templates have been reloaded!"));
return loadTemplate(INDEX,data,resp);
case RESET_PASSWORD:
if (!isGet) return resetPassword(req,resp);
// TODO: move following code into resetPassword method
var token = req.getParameter(TOKEN);
if (token != null){
try {
user = User.byToken(req.getParameter(TOKEN));
if (user == null) return loadTemplate(path,Map.of(ERROR,t("Failed to find user for token!")),resp);
user.dropPasswordToken();
req.getSession().setAttribute("user",user);
return redirectTo(NEW_PASSWORD_FORM,resp);
} catch (SQLException sqle){
return loadTemplate(path,Map.of(ERROR,t("Failed to add user for token!")),resp);
}
}
var email = req.getParameter(EMAIL);
return loadTemplate(path,email == null ? null : Map.of(EMAIL,email),resp);
data.put(NOTES,t("Templates have been reloaded"));
path = INDEX;
case CSS:
case INDEX:
return loadTemplate(path,data,resp);
case SUBSCRIBE:
if (!isGet) {
return subscribe(req, resp);
} // TODO: Code für GET-Request mit in subscribe-Methode verschieben
if (list.isOpenFor(user)) {
data.put(LIST,list.email());
return loadTemplate(path, data, resp);
}
return t("You are not allowed to subscribe to '{}'!",list.email());
case UNSUBSCRIBE:
return unsubscribe(req,resp,isGet);
}
/* uninteresting paths */
switch (path){
case "js":
resp.setContentType("text/javascript");
return loadTemplate(path,data,resp);
case LOGIN:
try {
if (User.noUsers()) return loadTemplate(REGISTER, Map.of(NOTES,t("User database is empty. Create admin user first:")), resp);
return loadTemplate(path,null,resp);
} catch (SQLException e) {
return "Error reading user database!";
}
case LOGOUT:
req.getSession().invalidate();
return redirectTo(INDEX,resp);
case "jquery":
resp.setContentType("text/javascript");
return loadFile("jquery-3.6.0.min.js",resp);
@ -415,16 +323,69 @@ public class Web extends TemplateServlet { @@ -415,16 +323,69 @@ public class Web extends TemplateServlet {
return loadFile("Bhineka.ttf",resp); case UNSUBSCRIBE:
data.put(LIST,list.email());
return loadTemplate(path,data,resp);
}
if (user != null){
if (list != null) data.put(LIST,req.getParameter(LIST));
switch (path){
case EDIT_LIST:
return editList(req,resp);
}
return loadTemplate(path,data,resp);
}
return redirectTo(LOGIN,resp);
}
private String inspect(HttpServletRequest req, HttpServletResponse resp, boolean isGet) {
private String handleLogin(HttpServletRequest req, HttpServletResponse resp) {
var email = req.getParameter("email");
var pass = req.getParameter("pass");
if (email == null || pass == null) return loadTemplate("login", Map.of("error",t("Missing username or password!")), resp);
if (!Util.isEmail(email)) return loadTemplate("login", Map.of("error",t("'{}' is not a valid email address!",email)), resp);
try {
var user = User.loadUser(email,pass);
req.getSession().setAttribute("user",user);
// loading user successfull: goto index
resp.sendRedirect(String.join("/",WEB_ROOT,"admin"));
} catch (Exception e) {
try {
LOG.warn("Static.handleLogin failed:",e);
Thread.sleep(10000);
} finally {
return loadTemplate("login", Map.of(ERROR,t("Invalid username/password"),EMAIL,email), resp);
}
}
return null;
}
private String handlePost(HttpServletRequest req, HttpServletResponse resp) {
final var path = Util.getPath(req);
switch (path){
case ADD_LIST:
return addList(req,resp);
case EDIT_LIST:
return editList(req,resp);
case INSPECT:
return inspect(req,resp);
case LOGIN:
return handleLogin(req,resp);
case REGISTER:
return registerUser(req,resp);
case SUBSCRIBE:
return subscribe(req,resp);
case UNSUBSCRIBE:
return unsubscribe(req,resp);
}
return t("No handler for path {}!",path);
}
private String inspect(HttpServletRequest req, HttpServletResponse resp) {
var user = Util.getUser(req);
if (user == null) return redirectTo(LOGIN,resp);
var data = new HashMap<String,Object>();
data.put(USER,user);
var error = false;
var list = Util.getMailingList(req);
@ -432,11 +393,10 @@ public class Web extends TemplateServlet { @@ -432,11 +393,10 @@ public class Web extends TemplateServlet {
error = true;
data.put(ERROR, t("No valid mailing list provided!"));
} else data.put(LIST, list.email());
if (isGet) return loadTemplate(INSPECT,data,resp);
if (!error && !list.mayBeAlteredBy(user)) {
error = true;
data.put(ERROR,t("You are not allowed to alter settings of this list!"));
data.put(ERROR,t("You are not alter settings of this list!"));
}
if (!error){
@ -485,8 +445,10 @@ public class Web extends TemplateServlet { @@ -485,8 +445,10 @@ public class Web extends TemplateServlet {
}
}
private String registerUser(HttpServletRequest req, HttpServletResponse resp, boolean isGet) {
if (isGet) return loadTemplate(REGISTER,null,resp);
private String registerUser(HttpServletRequest req, HttpServletResponse resp) {
var email = req.getParameter("email");
var pass = req.getParameter("pass");
var pass_repeat = req.getParameter("pass_repeat");
@ -495,9 +457,9 @@ public class Web extends TemplateServlet { @@ -495,9 +457,9 @@ public class Web extends TemplateServlet {
if (email == null || email.isBlank() ||
name == null || name.isBlank() ||
pass == null || pass.isBlank() ||
pass_repeat == null || pass_repeat.isBlank()) return loadTemplate(REGISTER,Map.of(ERROR,t("Fill all fields, please!"),NAME,name,EMAIL,email),resp);
if (!pass.equals(pass_repeat)) return loadTemplate(REGISTER,Map.of(ERROR,t("Passwords do not match!"),NAME,name,EMAIL,email),resp);
if (Util.simplePassword(pass)) return loadTemplate(REGISTER,Map.of(ERROR,t("Password to short or to simple!"),NAME,name,EMAIL,email),resp);
pass_repeat == null || pass_repeat.isBlank()) return loadTemplate(REGISTER,Map.of(ERROR,"Fill all fields, please!",NAME,name,EMAIL,email),resp);
if (!pass.equals(pass_repeat)) return loadTemplate(REGISTER,Map.of(ERROR,"Passwords do not match!",NAME,name,EMAIL,email),resp);
if (Util.simplePassword(pass)) return loadTemplate(REGISTER,Map.of(ERROR,"Password to short or to simple!",NAME,name,EMAIL,email),resp);
var firstUser = false;
try {
@ -506,6 +468,7 @@ public class Web extends TemplateServlet { @@ -506,6 +468,7 @@ public class Web extends TemplateServlet {
return t("Failed to access user database: {}",e.getMessage());
}
try {
var user = User.create(email, name, pass);
if (firstUser) user.addPermission(PERMISSION_ADMIN|User.PERMISSION_CREATE_LISTS);
@ -517,33 +480,6 @@ public class Web extends TemplateServlet { @@ -517,33 +480,6 @@ public class Web extends TemplateServlet {
}
}
private String resetPassword(HttpServletRequest req, HttpServletResponse resp) {
var email = req.getParameter("email");
if (email == null) return loadTemplate("login", Map.of("error",t("Missing email address!")), resp);
if (!Util.isEmail(email)) return loadTemplate("login", Map.of("error",t("'{}' is not a valid email address!",email)), resp);
try {
var user = User.load(email);
if (user != null) {
MailingList list = ListMember.listsOf(user).stream().map(ListMember::list).filter(ml -> ml.hasState(STATE_ENABLED)).findAny().orElse(null);
if (list == null) list = MailingList.listActive().stream().findAny().orElse(null);
if (list == null) throw new NullPointerException("no active List found!");
String token = user.generateToken();
var host = Arrays.stream(req.getRequestURL().toString().split("/")).filter(s -> !s.isEmpty() && !s.startsWith("http")).findFirst().orElse("unknwon host");
var link = req.getRequestURL().toString()+"?token="+token;
var subject = t("Reset your password at {}",host);
var text = t("Somebody asked to reset your account password at {}.\n\nIf that was you, please open the following link in your web browser:\n\n{}\n\nIf you did not expect this email, please ignore it.",host,link);
list.sendPasswordReset(email,subject,text);
return loadTemplate("reset_link_sent",null,resp);
}
} catch (Exception e){
return loadTemplate(Util.getPath(req),Map.of(EMAIL,email,ERROR,t("Failed to send reset email:")+" "+e.getMessage()),resp);
}
return null;
}
private String subscribe(HttpServletRequest req, HttpServletResponse resp) {
var name = req.getParameter(NAME);
var email = req.getParameter(EMAIL);
@ -575,7 +511,6 @@ public class Web extends TemplateServlet { @@ -575,7 +511,6 @@ public class Web extends TemplateServlet {
int code = cause.getErrorCode();
if (code == PRIMARY_KEY_CONSTRAINT) try {// user already exists
user = User.loadUser(email,pass);
req.getSession().setAttribute("user",user);
skipConfirmation = pass != null; // subscription with email address already known to database
// success → subscribe
} catch (InvalidKeyException | SQLException e) {
@ -614,21 +549,22 @@ public class Web extends TemplateServlet { @@ -614,21 +549,22 @@ public class Web extends TemplateServlet {
}
}
private String unsubscribe(HttpServletRequest req, HttpServletResponse resp, boolean isGet) {
private String unsubscribe(HttpServletRequest req, HttpServletResponse resp) {
var data = new HashMap<String,Object>();
var user = Util.getUser(req);
var user = getSessionUser(req);
var email = req.getParameter(EMAIL);
var list = Util.getMailingList(req);
data.put(EMAIL,email);
if (user != null) data.put(USER,user.safeMap());
if (list == null){
data.put(ERROR,t("No list provided by form data!"));
data.put(ERROR,"No list provided by form data!");
return loadTemplate(UNSUBSCRIBE,data,resp);
} else data.put(LIST,list.email());
if (isGet) return loadTemplate(UNSUBSCRIBE,data,resp);
if (user == null) {
if (email == null || email.isBlank()) {
data.put(ERROR, t("Email is required for list un-subscription!"));
data.put(ERROR, "Email is required for list un-subscription!");
return loadTemplate(UNSUBSCRIBE, data, resp);
}
var pass = req.getParameter(PASSWORD);
@ -639,7 +575,7 @@ public class Web extends TemplateServlet { @@ -639,7 +575,7 @@ public class Web extends TemplateServlet {
req.getSession().setAttribute(USER,user);
data.put(USER,user.safeMap());
} catch (InvalidKeyException | SQLException e) {
data.put(ERROR,t("Invalid email/password combination!"));
data.put(ERROR,"Invalid email/password combination!");
return loadTemplate(UNSUBSCRIBE,data,resp);
}
}
@ -665,6 +601,7 @@ public class Web extends TemplateServlet { @@ -665,6 +601,7 @@ public class Web extends TemplateServlet {
LOG.warn("Problem during unscubsription of {} from {}:",user.email(),list.email(),e);
data.put(ERROR,"Failed to unsubscribe!");
return loadTemplate(UNSUBSCRIBE,data,resp);
}
}
}

65
src/test/java/de/srsoftware/widerhall/UtilTest.java

@ -4,7 +4,6 @@ import junit.framework.TestCase; @@ -4,7 +4,6 @@ import junit.framework.TestCase;
import java.util.Map;
import java.util.TreeMap;
import static de.srsoftware.widerhall.Util.*;
public class UtilTest extends TestCase {
@ -18,17 +17,17 @@ public class UtilTest extends TestCase { @@ -18,17 +17,17 @@ public class UtilTest extends TestCase {
}
public void testHex(){
assertEquals("00",hex(0));
assertEquals("09",hex(9));
assertEquals("0A",hex(10));
assertEquals("0F",hex(15));
assertEquals("10",hex(16));
assertEquals("19",hex(25));
assertEquals("FF",hex(255));
assertEquals("00",Util.hex(0));
assertEquals("09",Util.hex(9));
assertEquals("0A",Util.hex(10));
assertEquals("0F",Util.hex(15));
assertEquals("10",Util.hex(16));
assertEquals("19",Util.hex(25));
assertEquals("FF",Util.hex(255));
}
public void testSha256() {
assertEquals("9F722959A023C02A3BA0FAFDBA81ADED642D6610EFF5DCA32DCE35132E16B6C5",sha256("Dies ist ein Test"));
assertEquals("9F722959A023C02A3BA0FAFDBA81ADED642D6610EFF5DCA32DCE35132E16B6C5",Util.sha256("Dies ist ein Test"));
}
public void testTranslate() {
@ -36,40 +35,30 @@ public class UtilTest extends TestCase { @@ -36,40 +35,30 @@ public class UtilTest extends TestCase {
}
public void testIsEmail() {
assertFalse(isEmail("Test"));
assertFalse(isEmail("Test@"));
assertFalse(isEmail("@Test"));
assertTrue(isEmail("Test@Domain"));
assertFalse(isEmail("Test@Domain@Test"));
assertFalse(Util.isEmail("Test"));
assertFalse(Util.isEmail("Test@"));
assertFalse(Util.isEmail("@Test"));
assertTrue(Util.isEmail("Test@Domain"));
assertFalse(Util.isEmail("Test@Domain@Test"));
}
public void testSimplePassword() {
assertTrue(simplePassword("$@%€#")); // too short
assertTrue(simplePassword("test123")); // no special chars
assertFalse(simplePassword("test$23")); // contains special chars
assertTrue(simplePassword("skgjafdsg")); // chars only
assertTrue(simplePassword("986535465")); // digits only
assertFalse(simplePassword("test9523")); // mixed digits and chars
assertFalse(simplePassword("8986546054")); // digits only, but long enough
assertFalse(simplePassword("salgksdjbw")); // chars only, but long enough
assertTrue(Util.simplePassword("$@%€#")); // too short
assertTrue(Util.simplePassword("test123")); // no special chars
assertFalse(Util.simplePassword("test$23")); // contains special chars
assertTrue(Util.simplePassword("skgjafdsg")); // chars only
assertTrue(Util.simplePassword("986535465")); // digits only
assertFalse(Util.simplePassword("test9523")); // mixed digits and chars
assertFalse(Util.simplePassword("8986546054")); // digits only, but long enough
assertFalse(Util.simplePassword("salgksdjbw")); // chars only, but long enough
}
public void testUnsetFlags(){
assertEquals(0,unset(31,1,2,4,8,16));
assertEquals(1,unset(31,2,4,8,16));
assertEquals(2,unset(31,1,4,8,16));
assertEquals(4,unset(31,1,2,8,16));
assertEquals(8,unset(31,1,2,4,16));
assertEquals(16,unset(31,1,2,4,8));
}
public void testBoundedChar(){
assertEquals('0',boundedChar(0));
assertEquals('9',boundedChar(9));
assertEquals('A',boundedChar(10));
assertEquals('Z',boundedChar(35));
assertEquals('a',boundedChar(36));
assertEquals('z',boundedChar(61));
assertEquals('0',boundedChar(62));
assertEquals(0,Util.unset(31,1,2,4,8,16));
assertEquals(1,Util.unset(31,2,4,8,16));
assertEquals(2,Util.unset(31,1,4,8,16));
assertEquals(4,Util.unset(31,1,2,8,16));
assertEquals(8,Util.unset(31,1,2,4,16));
assertEquals(16,Util.unset(31,1,2,4,8));
}
}

4
src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java

@ -110,7 +110,7 @@ public class ListMemberTest extends TestCase { @@ -110,7 +110,7 @@ public class ListMemberTest extends TestCase {
}*/
public void testSafeMap() {
var user = new User("email","name","salt","hash",null,0);
var user = new User("email","name","salt","hash",0);
var lm = new ListMember(null,user,ListMember.STATE_AWAITING_CONFIRMATION,"token");
assertEquals(Map.of(EMAIL,"email",NAME,"name",STATE,"awaiting confirmation"),lm.safeMap());
}
@ -137,7 +137,7 @@ public class ListMemberTest extends TestCase { @@ -137,7 +137,7 @@ public class ListMemberTest extends TestCase {
}*/
public void testUser() {
var user = new User("email","name","salt","hash",null,0);
var user = new User("email","name","salt","hash",0);
var lm = new ListMember(null,user,ListMember.STATE_AWAITING_CONFIRMATION,"token");
assertEquals(user,lm.user());
}

3
static/templates/archive.st

@ -18,9 +18,6 @@ @@ -18,9 +18,6 @@
<th>Date</th>
<th>From</th>
<th>Subject</th>
«if (data.moderator)»
<th>Actions</th>
«endif»
</tr>
</table>
<script type="text/javascript">

8
static/templates/footer.st

@ -1,7 +1,3 @@ @@ -1,7 +1,3 @@
<div class="footer">
Widerhall Mail Distributor.
Version 1.0.7.
Get the sources at <a target="_blank" href="https://git.srsoftware.de/StephanRichter/Widerhall">git.srsoftware.de</a>
</div>
Widerhall Mail Distributor. Get the sources at <a target="_blank" href="https://git.srsoftware.de/StephanRichter/Widerhall">git.srsoftware.de</a>
</div>

53
static/templates/js.st

@ -16,10 +16,6 @@ function dropList(listEmail){ @@ -16,10 +16,6 @@ function dropList(listEmail){
$.post('/api/list/drop',{list:listEmail},showListResult,'json');
}
function dropMail(mailId){
$.post('/api/mail/drop',{message_id:mailId},showListResult,'json');
}
function dropMember(userEmail,listEmail){
$.post('/api/list/drop_member',{list:listEmail,email:userEmail},reload,'json');
}
@ -82,8 +78,6 @@ function showList(listEmail){ @@ -82,8 +78,6 @@ function showList(listEmail){
function showListArchive(data){
console.log(data);
let moderator = data.archive.moderator;
console.log('moderator: ',moderator);
let posts = data.archive.posts;
for (let time in posts){
let post = posts[time];
@ -92,9 +86,6 @@ function showListArchive(data){ @@ -92,9 +86,6 @@ function showListArchive(data){
$('<td/>').html('<a href="'+url+'">'+post.date+'</a>').appendTo(row);
$('<td/>').html('<a href="'+url+'">'+post.from_name+'</a>').appendTo(row);
$('<td/>').html('<a href="'+url+'">'+post.subject+'</a>').appendTo(row);
if (moderator){
$('<button/>',{onclick:"dropMail('"+post.id+"');"}).text('Delete').appendTo($('<td/>')).appendTo(row);
}
row.appendTo($('#archive table'));
}
}
@ -141,24 +132,13 @@ function showListOfModeratedLists(data){ @@ -141,24 +132,13 @@ function showListOfModeratedLists(data){
$('<td/>').html('<span class="error">'+list.last_error+'</span>').appendTo(row);
} else $('<td/>').text('-').appendTo(row);
let select = $('<select/>',{name:addr}).change(function () {
let selected = $(this).children("option:selected");
let action = selected.val();
let verb = selected.text();
let action = $(this).children("option:selected").val();
let list = $(this).attr('name');
if (confirm("This will "+verb+" '"+list+"'. Are you sure?")) self[action+'List'](list);
if (confirm("This will "+action+" '"+list+"'. Are you sure?")) self[action+'List'](list);
});
$('<option/>').text('Actions').appendTo(select);
['enable','disable','drop','hide','show','test'].forEach(val => $('<option/>',{value:val}).text(val).appendTo(select));
Object.entries({
'enable':'enable',
'disable':'disable',
'drop':'drop',
'hide':'hide',
'show':'show',
'test':'test'
}).forEach(([k,v],i) => {
$('<option/>',{value:k}).text(v).appendTo(select)
});
select.appendTo($('<td/>')).appendTo(row);
$('<td/>').text(list.imap_host).appendTo(row);
@ -211,11 +191,6 @@ function showListResult(result){ @@ -211,11 +191,6 @@ function showListResult(result){
function showMembers(data){
var list_mail = data.list.email.prefix+'@'+data.list.email.domain;
console.log("data",data);
var owner = false;
for (let member of data.members){
if (member.email == data.user.email && member.state.includes("owner")) owner = member.email;
}
for (let i in data.members){
let member = data.members[i];
let row = $('<tr/>');
@ -223,17 +198,11 @@ function showMembers(data){ @@ -223,17 +198,11 @@ function showMembers(data){
$('<td/>').text(member.email).appendTo(row);
$('<td/>').text(member.state).appendTo(row);
let col = $('<td/>');
console.log("data",data);
if (member.state.includes("moderator")) {
if (member.email != owner){
$('<button/>',{onclick:'dropMod("'+member.email+'","'+list_mail+'")'}).text("- moderator").appendTo(col);
if (data.user.email == owner) $('<button/>',{onclick:'transfer("'+member.email+'","'+list_mail+'")'}).text("transfer ownership").appendTo(col);
}
} else {
$('<button/>',{onclick:'addMod("'+member.email+'","'+list_mail+'")'}).text("+ moderator").appendTo(col);
}
if (!member.state.includes("owner")) {
$('<button/>',{onclick:'dropMember("'+member.email+'","'+list_mail+'")'}).text("remove").appendTo(col);
}
if (!member.state.includes("owner")) $('<button/>',{onclick:'dropMod("'+member.email+'","'+list_mail+'")'}).text("- moderator").appendTo(col);
} else $('<button/>',{onclick:'addMod("'+member.email+'","'+list_mail+'")'}).text("+ moderator").appendTo(col);
if (!member.state.includes("owner")) $('<button/>',{onclick:'dropMember("'+member.email+'","'+list_mail+'")'}).text("remove").appendTo(col);
col.appendTo(row);
row.appendTo('#memberlist');
@ -284,15 +253,9 @@ function testList(listEmail){ @@ -284,15 +253,9 @@ function testList(listEmail){
$.post('/api/list/test',{list:listEmail},showListResult,'json');
}
function transfer(newOwner,listEmail){
if (confirm("Really transfer "+listEmail+" to "+newOwner+"?")){
$.post('/api/list/transfer',{email:newOwner,list:listEmail},reload,'json');
}
}
function unsubscribeFrom(listEmail){
window.location.href='unsubscribe?list='+listEmail;
}
$(start); // document.on ready
$(start); // document.on ready

7
static/templates/navigation.st

@ -1,12 +1,7 @@ @@ -1,12 +1,7 @@
<nav>
«if(data.user)»
<a class="button logout" href="logout" />Logout</a>
«endif»
«if(data.user)»<a class="button logout" href="logout" />Logout</a>«endif»
<a href="index">Home</a>
<a href="admin">Administration</a>
«if(!data.user)»
<a class="button" href="reset-pw" />Forgot password?</a>
«endif»
<script type="text/javascript">
if ('«data.user.permissions»'.includes('admin')){

33
static/templates/new_password_form.st

@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
«head()»
«userinfo()»
<body id="login">
«navigation()»
«messages()»
<h1>Set new account password</h1>
<form method="POST">
<fieldset>
Dear «if(data.user.name)»«data.user.name»«else»«data.user.email»«endif», you may now set a new password for your account «data.email»:
<legend>Login credentials</legend>
<label>
<input enabled="false" value="«data.user.email»" />
Email address
</label>
<label>
<input type="password" name="password" value="" id="password" />
Password
</label>
<label>
<input type="password" name="password-repeat" value="" id="password-repeat" />
Password (repeat)
</label>
<input type="hidden" name="token" value="«data.token»" />
<button type="submit">Save new password</button>
</fieldset>
</form>
<a href="reset-pw">Forgot password?</a>
</body>
</html>

21
static/templates/reset-pw.st

@ -1,21 +0,0 @@ @@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
«head()»
<body id="login">
«navigation()»
«messages()»
<h1>Widerhall password recovery</h1>
<form method="POST">
<fieldset>
<legend>Forgot password?</legend>
If you have lost or forgot your password, please enter your email below.
Upon pressing the "Reset password" button, you will be sent an email with a link allowing you to set a new password.
<label>
<input type="text" name="email" value="«data.email»" id="email" />
Email address
</label>
<button type="submit">Reset password</button>
</fieldset>
</form>
</body>
</html>

17
static/templates/reset_link_sent.st

@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
«head()»
<body id="login">
«navigation()»
«messages()»
<h1>Confirmation</h1>
<fieldset>
<legend>
Link sent
</legend>
An email was sent to you:<br/>
This mail contains a link, that proves you are in control about the resprective mail address. Open the received link in a web browser and you will be able to set a new password.
</fieldset>
</body>
</html>

4
static/templates/subscribe.st

@ -11,11 +11,11 @@ @@ -11,11 +11,11 @@
<fieldset>
<legend>Suscribe to "«data.list»"</legend>
<label>
<input type="text" name="name" value="«if(data.user)»«data.user.name»«else»«data.name»«endif»">
<input type="text" name="name" value="«if(data.name)»«data.name»«else»«data.user.name»«endif»">
Name
</label>
<label>
<input type="text" name="email" value="«if(data.user)»«data.user.email»«else»«data.email»«endif»">
<input type="text" name="email" value="«if(data.email)»«data.email»«else»«data.user.email»«endif»">
Email
</label>
<label>

Loading…
Cancel
Save