Browse Source

working on list administration

drop_old_mail
Stephan Richter 3 years ago
parent
commit
1ef430b783
  1. 8
      src/main/java/de/srsoftware/widerhall/Application.java
  2. 1
      src/main/java/de/srsoftware/widerhall/Constants.java
  3. 7
      src/main/java/de/srsoftware/widerhall/Util.java
  4. 17
      src/main/java/de/srsoftware/widerhall/data/Database.java
  5. 36
      src/main/java/de/srsoftware/widerhall/data/ListMember.java
  6. 67
      src/main/java/de/srsoftware/widerhall/data/MailingList.java
  7. 2
      src/main/java/de/srsoftware/widerhall/data/User.java
  8. 29
      src/main/java/de/srsoftware/widerhall/web/Web.java
  9. 9
      src/test/java/de/srsoftware/widerhall/UtilTest.java
  10. 8
      src/test/java/de/srsoftware/widerhall/data/DatabaseTest.java
  11. 2
      static/templates/login.st

8
src/main/java/de/srsoftware/widerhall/Application.java

@ -1,21 +1,15 @@
package de.srsoftware.widerhall; package de.srsoftware.widerhall;
import de.srsoftware.widerhall.mail.Forwarder;
import de.srsoftware.widerhall.mail.ImapClient;
import de.srsoftware.widerhall.mail.MessageHandler;
import de.srsoftware.widerhall.web.Web;
import de.srsoftware.widerhall.web.Rest; import de.srsoftware.widerhall.web.Rest;
import de.srsoftware.widerhall.web.Web;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.json.simple.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.file.Path;
public class Application { public class Application {
private static final Logger LOG = LoggerFactory.getLogger(Application.class); private static final Logger LOG = LoggerFactory.getLogger(Application.class);

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

@ -21,6 +21,7 @@ public class Constants {
public static final String PREFIX = "prefix"; public static final String PREFIX = "prefix";
public static final String PROTOCOL = "mail.store.protocol"; public static final String PROTOCOL = "mail.store.protocol";
public static final String STATE = "state"; public static final String STATE = "state";
public static final String TOKEN = "token";
public static final String USER = "user"; public static final String USER = "user";
public static final String VARCHAR = "VARCHAR(255)"; public static final String VARCHAR = "VARCHAR(255)";

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

@ -83,4 +83,11 @@ public class Util {
} }
return false; return false;
} }
public static int unset(int value, int...flags) {
for (int flag : flags){
if ((value & flag) > 0) value ^= flag;
}
return value;
}
} }

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

@ -70,10 +70,10 @@ public class Database {
var keys = new ArrayList<String>(); var keys = new ArrayList<String>();
var expressions = new ArrayList<String>(); var expressions = new ArrayList<String>();
for (var entry : setValues.entrySet()) { for (var entry : setValues.entrySet()) {
expressions.add(" SET "+entry.getKey()+" = ?"); expressions.add(entry.getKey()+" = ?");
args.add(entry.getValue()); args.add(entry.getValue());
} }
sql.append(String.join(", ",expressions)); sql.append(" SET ").append(String.join(", ",expressions));
} }
if (!values.isEmpty()){ if (!values.isEmpty()){
@ -226,11 +226,12 @@ public class Database {
} }
} }
public Request update(String tableName,String ...expressions) { public Request update(String tableName) {
var sql = new StringBuilder("UPDATE ").append(tableName); return new Request(new StringBuilder("UPDATE ").append(tableName));
if (expressions != null && expressions.length > 0) { }
sql.append(" SET ").append(String.join(", ",expressions));
} public static String xor(Object a, Object b){
return new Request(sql); // https://stackoverflow.com/a/16443025/1285585
return "(~("+a+"&"+b+"))&("+a+"|"+b+")";
} }
} }

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

@ -5,16 +5,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.stringtemplate.v4.ST; import org.stringtemplate.v4.ST;
import javax.xml.crypto.Data;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Constants.STATE;
public class ListMember { public class ListMember {
private static final Logger LOG = LoggerFactory.getLogger(ListMember.class); private static final Logger LOG = LoggerFactory.getLogger(ListMember.class);
@ -25,7 +19,6 @@ public class ListMember {
private static final String LIST_EMAIL = "list_email"; private static final String LIST_EMAIL = "list_email";
private static final String USER_EMAIL = "user_email"; private static final String USER_EMAIL = "user_email";
private static final String STATE = "state"; private static final String STATE = "state";
private static final String TOKEN = "token";
private final String listEmail,token,userEmail; private final String listEmail,token,userEmail;
private final int state; private final int state;
@ -36,6 +29,27 @@ public class ListMember {
this.token = token; this.token = token;
} }
public static User confirm(String token) throws SQLException {
var rs = Database.open().select(TABLE_NAME).where(TOKEN,token).exec();
while (rs.next()){
var lm = new ListMember(rs.getString(LIST_EMAIL),rs.getString(USER_EMAIL),rs.getInt(STATE),rs.getString(TOKEN));
User user = User.loadAll(List.of(lm.userEmail)).stream().findAny().orElse(null);
if (user != null){
int newState = lm.state ^ STATE_AWAITING_CONFIRMATION | STATE_SUBSCRIBER;
Database.open()
.update(TABLE_NAME)
.set(TOKEN,"NULL")
.set(STATE, newState) //drop confirmation state, set subscriber state
.where(LIST_EMAIL,lm.listEmail)
.where(USER_EMAIL,lm.userEmail)
.run();
}
return user;
}
return null;
}
public static ListMember create(MailingList list, User user, int state) throws SQLException { public static ListMember create(MailingList list, User user, int state) throws SQLException {
String token = null; String token = null;
if ((state & STATE_AWAITING_CONFIRMATION) > 0){ if ((state & STATE_AWAITING_CONFIRMATION) > 0){
@ -56,8 +70,8 @@ public class ListMember {
Database.open().query(sql).run(); Database.open().query(sql).run();
} }
public static List<String> listsOwnedBy(User user) { public static Set<String> listsOwnedBy(User user) {
var list = new ArrayList<String>(); var list = new HashSet<String>();
try { try {
var rs = Database.open().select(TABLE_NAME,LIST_EMAIL) var rs = Database.open().select(TABLE_NAME,LIST_EMAIL)
.where(USER_EMAIL,user.email()) .where(USER_EMAIL,user.email())
@ -113,7 +127,7 @@ public class ListMember {
.where(USER_EMAIL,user.email()) .where(USER_EMAIL,user.email())
.exec(); .exec();
while (rs.next()){ while (rs.next()){
int state = rs.getInt(STATE) ^ STATE_SUBSCRIBER; int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_AWAITING_CONFIRMATION);
if (state < 1) { // drop entry if (state < 1) { // drop entry
db.deleteFrom(TABLE_NAME) db.deleteFrom(TABLE_NAME)
.where(LIST_EMAIL,list.email()) .where(LIST_EMAIL,list.email())

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

@ -8,10 +8,7 @@ import org.slf4j.LoggerFactory;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Constants.*;
import static de.srsoftware.widerhall.Util.t; import static de.srsoftware.widerhall.Util.t;
@ -78,48 +75,28 @@ public class MailingList {
public static void enable(String listEmail, boolean enable) throws SQLException { public static void enable(String listEmail, boolean enable) throws SQLException {
// https://stackoverflow.com/questions/16440831/bitwise-xor-in-sqlite-bitwise-not-not-working-as-i-expect Database.open()
String expression = enable ? "state = state | "+ STATE_ENABLED : "state = (~(state & "+ STATE_ENABLED +"))&(state|"+ STATE_ENABLED +")"; .update(TABLE_NAME)
Database.open().update(TABLE_NAME,expression).where(EMAIL, listEmail).run(); .set(STATE,enable ? STATE+" | "+ STATE_ENABLED : Database.xor(STATE,STATE_ENABLED))
.where(EMAIL, listEmail).run();
} }
public static void hide(String listEmail, boolean hide) throws SQLException { public static void hide(String listEmail, boolean hide) throws SQLException {
// https://stackoverflow.com/questions/16440831/bitwise-xor-in-sqlite-bitwise-not-not-working-as-i-expect Database.open()
String expression = hide ? "state = (~(state & "+ STATE_PUBLIC +"))&(state|"+ STATE_PUBLIC +")" : ("state = state | "+ STATE_PUBLIC); .update(TABLE_NAME)
Database.open().update(TABLE_NAME,expression).where(EMAIL, listEmail).run(); .set(STATE,hide ? STATE+" | "+ STATE_PUBLIC : Database.xor(STATE,STATE_PUBLIC))
.where(EMAIL, listEmail).run();
} }
public static boolean isOpen(String list) { public static boolean isOpen(String list) {
return openLists().stream().filter(ml -> ml.email.equals(list)).count() > 0; return openLists().stream().filter(ml -> ml.email.equals(list)).count() > 0;
} }
public static List<MailingList> listsOf(User user) { public static Set<MailingList> listsOf(User user) {
var list = openLists();
List<String> keys = (user.is(ADMIN)) ? null : ListMember.listsOwnedBy(user); Set<String> keys = (user.is(ADMIN)) ? null : ListMember.listsOwnedBy(user);
var list = new ArrayList<MailingList>(); if (keys == null || keys.isEmpty()) return list;
if (keys != null && keys.isEmpty()) return list; for (String key : keys) list.add(load(key));
try {
Database.Request query = Database.open().select(TABLE_NAME);
if (keys != null) query.where(EMAIL, keys);
var rs = query.exec();
while (rs.next()) {
var email = rs.getString(EMAIL);
var name = rs.getString(NAME);
var imapHost = rs.getString(IMAP_HOST);
var imapPort = rs.getInt(IMAP_PORT);
var imapUser = rs.getString(IMAP_USER);
var imapPass = rs.getString(IMAP_PASS);
var smtpHost = rs.getString(SMTP_HOST);
var smtpPort = rs.getInt(SMTP_PORT);
var smtpUser = rs.getString(SMTP_USER);
var smtpPass = rs.getString(SMTP_PASS);
var state = rs.getInt(STATE);
list.add(new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, smtpHost, smtpPort, smtpUser, smtpPass, state));
}
} catch (SQLException e) {
LOG.warn("Listing mailing lists failed: ", e);
}
return list; return list;
} }
@ -155,19 +132,17 @@ public class MailingList {
return name; return name;
} }
public static List<MailingList> openLists() { public static Set<MailingList> openLists() {
var list = new ArrayList<MailingList>(); var list = new HashSet<MailingList>();
try { try {
var rs = Database.open() var rs = Database.open()
.select(TABLE_NAME,"*", "(" + STATE + " & " + STATE_PUBLIC + ") as test") .select(TABLE_NAME,EMAIL, "(" + STATE + " & " + STATE_PUBLIC + ") as test")
.where("test", STATE_PUBLIC) .where("test", STATE_PUBLIC)
.exec(); .exec();
while (rs.next()) { var emails = new ArrayList<String>();
var email = rs.getString(EMAIL); while (rs.next()) emails.add(rs.getString(EMAIL));
var name = rs.getString(NAME); rs.close();
var state = rs.getInt(STATE); for (String email : emails) list.add(load(email));
list.add(new MailingList(email, name, null, 0, null, null, null, 0, null, null, state));
}
} catch (SQLException e) { } catch (SQLException e) {
LOG.warn("Listing mailing lists failed: ", e); LOG.warn("Listing mailing lists failed: ", e);
} }

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

@ -117,7 +117,7 @@ public class User {
private boolean matching(String password) { private boolean matching(String password) {
if (hashedPass == null && password == null) return true; if (hashedPass == null) return password == null;
return hashedPass.equals(Util.sha256(password+salt)); return hashedPass.equals(Util.sha256(password+salt));
} }

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

@ -28,13 +28,14 @@ import static de.srsoftware.widerhall.Util.t;
public class Web extends HttpServlet { public class Web extends HttpServlet {
private static final String ADD_LIST = "add_list"; private static final String ADD_LIST = "add_list";
private static final String CONFIRM = "confirm";
private static final Logger LOG = LoggerFactory.getLogger(Web.class); private static final Logger LOG = LoggerFactory.getLogger(Web.class);
private static final String LOGIN = "login"; private static final String LOGIN = "login";
private static final String LOGOUT = "logout"; private static final String LOGOUT = "logout";
private static final String REGISTER = "register"; private static final String REGISTER = "register";
private static final String RELOAD = "reload";
private static final String SUBSCRIBE = "subscribe"; private static final String SUBSCRIBE = "subscribe";
private static final String UNSUBSCRIBE = "unsubscribe"; private static final String UNSUBSCRIBE = "unsubscribe";
private static final String RELOAD = "reload";
private static final String IMAP_HOST = "imap_host"; private static final String IMAP_HOST = "imap_host";
private static final String IMAP_PORT = "imap_port"; private static final String IMAP_PORT = "imap_port";
private static final String IMAP_USER = "imap_user"; private static final String IMAP_USER = "imap_user";
@ -128,6 +129,19 @@ public class Web extends HttpServlet {
} }
private String confirm(HttpServletRequest req, HttpServletResponse resp) {
try {
var token = req.getParameter(TOKEN);
if (token== null || token.isBlank()) return t("Invalid or missing token!");
var user = ListMember.confirm(token);
if (user != null) return loadTemplate(INDEX,Map.of(USER,user.safeMap(),NOTES,"Confirmed list subscription!"),resp);
return t("Unknown user");
} catch (SQLException e) {
LOG.debug("Failed to confirm list membership:",e);
return t("Confirmation of list membership failed!");
}
}
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
@ -150,6 +164,10 @@ public class Web extends HttpServlet {
return sqle; return sqle;
} }
private User getSessionUser(HttpServletRequest req) {
return req.getSession().getAttribute(USER) instanceof User user ? user : null;
}
private String handleGet(HttpServletRequest req, HttpServletResponse resp) { private String handleGet(HttpServletRequest req, HttpServletResponse resp) {
var o = req.getSession().getAttribute("user"); var o = req.getSession().getAttribute("user");
User user = o instanceof User ? (User) o : null; User user = o instanceof User ? (User) o : null;
@ -161,6 +179,8 @@ public class Web extends HttpServlet {
var list = req.getParameter(LIST); var list = req.getParameter(LIST);
if (list != null && !list.isBlank()) data.put(LIST,list); if (list != null && !list.isBlank()) data.put(LIST,list);
switch (path){ switch (path){
case CONFIRM:
return confirm(req,resp);
case RELOAD: case RELOAD:
loadTemplates(); loadTemplates();
data.put(NOTES,t("Templates have been reloaded")); data.put(NOTES,t("Templates have been reloaded"));
@ -203,6 +223,7 @@ public class Web extends HttpServlet {
} }
private String handleLogin(HttpServletRequest req, HttpServletResponse resp) { private String handleLogin(HttpServletRequest req, HttpServletResponse resp) {
var email = req.getParameter("email"); var email = req.getParameter("email");
var pass = req.getParameter("pass"); var pass = req.getParameter("pass");
@ -218,7 +239,7 @@ public class Web extends HttpServlet {
LOG.warn("Static.handleLogin failed:",e); LOG.warn("Static.handleLogin failed:",e);
Thread.sleep(10000); Thread.sleep(10000);
} finally { } finally {
return loadTemplate("login", Map.of("error",t("Invalid username/password")), resp); return loadTemplate("login", Map.of(ERROR,t("Invalid username/password"),EMAIL,email), resp);
} }
} }
return null; return null;
@ -429,7 +450,5 @@ public class Web extends HttpServlet {
} }
} }
private User getSessionUser(HttpServletRequest req) {
return req.getSession().getAttribute(USER) instanceof User user ? user : null;
}
} }

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

@ -52,4 +52,13 @@ public class UtilTest extends TestCase {
assertFalse(Util.simplePassword("8986546054")); // digits only, but long enough assertFalse(Util.simplePassword("8986546054")); // digits only, but long enough
assertFalse(Util.simplePassword("salgksdjbw")); // chars only, but long enough assertFalse(Util.simplePassword("salgksdjbw")); // chars only, but long enough
} }
public void testUnsetFlags(){
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));
}
} }

8
src/test/java/de/srsoftware/widerhall/data/DatabaseTest.java

@ -51,7 +51,11 @@ public class DatabaseTest extends TestCase {
public void testUpdate(){ public void testUpdate(){
assertEquals("UPDATE Test",Database.open().update("Test").sql()); assertEquals("UPDATE Test",Database.open().update("Test").sql());
assertEquals("UPDATE Test SET x = 5",Database.open().update("Test","x = 5").sql()); assertEquals("UPDATE Test SET x = 5",Database.open().update("Test").set("x",5).sql());
assertEquals("UPDATE Test SET x = 5, y = 6",Database.open().update("Test","x = 5","y = 6").sql()); assertEquals("UPDATE Test SET x = 5, y = 6",Database.open().update("Test").set("x",5).set("y",6).sql());
}
public void testXor(){
assertEquals("(~(a&b))&(a|b)",Database.xor("a","b"));
} }
} }

2
static/templates/login.st

@ -13,7 +13,7 @@
<fieldset> <fieldset>
<legend>Login-Daten</legend> <legend>Login-Daten</legend>
<label> <label>
<input type="text" name="email" value="" id="email" /> <input type="text" name="email" value="«data.email»" id="email" />
E-Mail-Adresse E-Mail-Adresse
</label> </label>
<label> <label>

Loading…
Cancel
Save