diff --git a/config/logback.xml b/config/logback.xml deleted file mode 100644 index 49a6adb..0000000 --- a/config/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5}: %msg%n - - - - - - - - - - - diff --git a/pom.xml b/pom.xml index f1c8a40..875c21e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.2.13 + 0.2.16 diff --git a/src/main/java/de/srsoftware/widerhall/data/Database.java b/src/main/java/de/srsoftware/widerhall/data/Database.java index a495e47..3fea9e5 100644 --- a/src/main/java/de/srsoftware/widerhall/data/Database.java +++ b/src/main/java/de/srsoftware/widerhall/data/Database.java @@ -45,13 +45,13 @@ public class Database { * @throws SQLException */ public ResultSet exec() throws SQLException { - LOG.debug("Executing {}",this); + LOG.debug("Führe {} aus",this); try { var stmt = conn.prepareStatement(sql); for (int i = 0; i < args.size(); i++) stmt.setObject(i+1, args.get(i)); return stmt.executeQuery(); } catch (SQLException sqle) { // add sql to the exception - throw new SQLException(t("Query '{}' failed:",this),sqle); + throw new SQLException(t("Query '{}' fehlgeschlagen:",this),sqle); } } diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index 299687f..0b9e66a 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -1,17 +1,16 @@ package de.srsoftware.widerhall.data; import de.srsoftware.widerhall.Util; -import org.antlr.runtime.MismatchedTokenException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.stringtemplate.v4.ST; +import javax.ws.rs.HEAD; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import static de.srsoftware.widerhall.Constants.*; -import static de.srsoftware.widerhall.Constants.STATE; +import static de.srsoftware.widerhall.Util.t; /** * @author Stephan Richter @@ -22,6 +21,7 @@ public class ListMember { public static final int STATE_OWNER = 1; public static final int STATE_SUBSCRIBER = 2; public static final int STATE_AWAITING_CONFIRMATION = 4; + public static final int STATE_MODERATOR = 8; private static final Logger LOG = LoggerFactory.getLogger(ListMember.class); private static final String LIST_EMAIL = "list_email"; private static final String USER_EMAIL = "user_email"; @@ -46,6 +46,37 @@ public class ListMember { this.token = token; } + public String addNewModerator(String userEmail) { + if (!isAllowedToEditMods()) return t("Es ist dir nicht gestattet, neue Moderatoren für {} zu ernennen!",list.email()); + User moderator = null; + try { + moderator = User.load(userEmail); + } catch (SQLException e) { + LOG.warn("Laden des Nutzers zu {} fehlgeschlagen",userEmail,e); + return t("Laden des Nutzers zu {} fehlgeschlagen",userEmail); + } + if (moderator == null) return t("Kein solcher Nutzer: {}",userEmail); + + ListMember member = null; + try { + member = ListMember.load(list,moderator); + } catch (SQLException e) { + LOG.warn("Laden der Mitgliedschaft zu {}/{} fehlgeschlagen",moderator.email(),list.email(),e); + return t("Laden der Mitgliedschaft zu {}/{} fehlgeschlagen",moderator.email(),list.email()); + } + try { + if (member == null) { + ListMember.create(list, moderator, ListMember.STATE_MODERATOR); + } else { + member.setState(ListMember.STATE_MODERATOR); + } + } catch (SQLException e) { + LOG.warn("Ernennen von {} zum Moderator von {} fehlgeschlagen",moderator.email(),list.email(),e); + return t("Ernennen von {} zum Moderator von {} fehlgeschlagen",moderator.email(),list.email()); + } + return null; + } + /** * tries to confirm the token: * This method loads the list member, that is assigned with the token. @@ -112,6 +143,68 @@ public class ListMember { Database.open().query(sql).compile().run(); } + public String dropMember(String userEmail) { + if (!isModerator()) return t("Es ist dir nicht erlaubt, Mitglieder von {} zu entfernen.",list.email()); + User user = null; + try { + user = User.load(userEmail); + } catch (SQLException e) { + LOG.warn("Laden des Nutzers zu {} fehlgeschlagen",userEmail,e); + return t("Laden des Nutzers zu {} fehlgeschlagen",userEmail); + } + if (user == null) return t("Kein solcher Nutzer: {}",userEmail); + + ListMember member = null; + try { + member = ListMember.load(list,user); + } catch (SQLException e) { + LOG.warn("Laden der Mitgliedschaft zu {}/{} fehlgeschlagen",user.email(),list.email(),e); + return t("Laden der Mitgliedschaft zu {}/{} fehlgeschlagen",user.email(),list.email()); + } + + if (member == null) return t("{} it nicht Mitglied von {}",user.email(),list.email()); + if (member.isOwner()) return t("Du kannst den Listen-Besitzer nicht entfernen!"); + + try { + member.unsubscribe(); + } catch (SQLException e) { + LOG.warn("Abbestellen von {} / {} nicht fehlgeschlagen",user.email(),list.email(),e); + return t("Abbestellen von {} / {} nicht fehlgeschlagen",user.email(),list.email()); + } + return null; + } + + public String dropModerator(String userEmail) { + if (!isAllowedToEditMods()) return t("Es ist dir nicht gestattet, die Moderatoren von {} zu verändern",list.email()); + User moderator = null; + try { + moderator = User.load(userEmail); + } catch (SQLException e) { + LOG.warn("Laden des Nutzers zu {} fehlgeschlagen",userEmail,e); + return t("Laden des Nutzers zu {} fehlgeschlagen",userEmail); + } + if (moderator == null) return t("No such user: {}",userEmail); + + ListMember member = null; + try { + member = ListMember.load(list,moderator); + } catch (SQLException e) { + LOG.warn("Laden der Mitgliedschaft zu {}/{} fehlgeschlagen",moderator.email(),list.email(),e); + return t("Laden der Mitgliedschaft zu {}/{} fehlgeschlagen",moderator.email(),list.email()); + } + try { + if (member == null) { + ListMember.create(list, moderator, ListMember.STATE_SUBSCRIBER); + } else { + member.setState(ListMember.STATE_SUBSCRIBER); + } + } catch (SQLException e) { + LOG.warn("Ernennen von {} zum normalen Abonnenten von {} fehlgeschlagen",moderator.email(),list.email(),e); + return t("Ernennen von {} zum normalen Abonnenten von {} fehlgeschlagen",moderator.email(),list.email()); + } + return null; + } + /** * create a new ListMember object from a ResultSet * @param rs @@ -135,11 +228,58 @@ public class ListMember { return (state & testState) > 0; } + public boolean isAllowedToEditMods(){ + if (isOwner()) return true; + if (isModerator()) return list.modsMayEditMods(); + return false; + } + + public boolean isAwaiting(){ + return hasState(STATE_AWAITING_CONFIRMATION); + } + + public boolean isModerator() { + return hasState(STATE_OWNER|STATE_MODERATOR); + } public boolean isOwner(){ return hasState(STATE_OWNER); } + public boolean isSubscriber(){ + return hasState(STATE_SUBSCRIBER|STATE_MODERATOR|STATE_OWNER); + } + + public MailingList list(){ + return list; + } + /** + * return a set of list emails of MailingLists the given user attends + * @param user + * @return + */ + public static Set listsOf(User user) { + var listEmails = new HashSet(); + try { + var request = Database.open() + .select(TABLE_NAME); + if (!user.hashPermission(User.PERMISSION_ADMIN)) request = request.where(USER_EMAIL, user.email()); + var rs = request.compile().exec(); + while (rs.next()) listEmails.add(rs.getString(LIST_EMAIL)); + } catch (SQLException e) { + LOG.warn("Sammeln der Listen von {} fehlgeschlagen: ",user.email(),e); + } + var lists = MailingList.loadAll(listEmails); + var result = new HashSet(); + try { + for (var ml : lists) result.add(ListMember.load(ml,user)); + } catch (SQLException e) { + e.printStackTrace(); + } + + return result; + } + /** * return a set of list emails of MailingLists owned by the given user * @param user @@ -197,7 +337,7 @@ public class ListMember { return Map.of( EMAIL,user.email(), NAME,user.name(), - STATE,ListMember.stateText(state) + STATE,stateText() ); } @@ -217,16 +357,27 @@ public class ListMember { return this; } + public ListMember setState(int newState) throws SQLException { + Database.open() + .update(TABLE_NAME) + .set(STATE,newState) + .where(USER_EMAIL,user.email()) + .where(LIST_EMAIL,list.email()) + .compile() + .run(); + return this; + } + /** * convert state flag to readable text - * @param state * @return */ - public static String stateText(int state) { + public String stateText() { var words = new ArrayList(); - if ((state & STATE_OWNER) > 0) words.add("Besitzer"); - if ((state & STATE_SUBSCRIBER) > 0) words.add("Abonniert"); - if ((state & STATE_AWAITING_CONFIRMATION) > 0) words.add("erwartet Bestätigung"); + if (isSubscriber()) words.add("Abonniert"); + if (isOwner()) words.add("Besitzer"); + if (isAwaiting()) words.add("erwartet Bestätigung"); + if (isModerator()) words.add("Moderator"); return String.join(", ",words); } @@ -240,11 +391,9 @@ public class ListMember { /** * unsubscribe a list member - * @param list - * @param user * @throws SQLException */ - public static void unsubscribe(MailingList list, User user) throws SQLException { + public void unsubscribe() throws SQLException { var db = Database.open(); var rs = db.select(TABLE_NAME) .where(LIST_EMAIL,list.email()) @@ -252,7 +401,7 @@ public class ListMember { .compile() .exec(); while (rs.next()){ - int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_AWAITING_CONFIRMATION); // drop subscription and awaiting flags + int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_MODERATOR,STATE_AWAITING_CONFIRMATION); // drop subscription and awaiting flags var req = state < 1 ? db.deleteFrom(TABLE_NAME) : db.update(TABLE_NAME).set(STATE,state).set(TOKEN,null); req.where(LIST_EMAIL,list.email()).where(USER_EMAIL,user.email()).compile().run(); } diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index bf12901..cab8a52 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -1,11 +1,9 @@ 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.SmtpClient; -import org.json.simple.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +29,14 @@ import static de.srsoftware.widerhall.data.User.PERMISSION_ADMIN; * this class encapsulates a MailingList db object */ public class MailingList implements MessageHandler { + public static final String KEY_FORWARD_FROM = "forward_from"; + public static final String KEY_FORWARD_ATTACHED = "forward_attached"; + public static final String KEY_HIDE_RECEIVERS = "hide_receivers"; + public static final String KEY_REPLY_TO_LIST = "reply_to_list"; + public static final String KEY_OPEN_FOR_GUESTS = "open_for_guests"; + public static final String KEY_OPEN_FOR_SUBSCRIBERS = "open_for_subscribers"; + public static final String KEY_ARCHIVE = "archive"; + public static final String KEY_MODS_CAN_EDIT_MODS = "edit_mods"; private static final Logger LOG = LoggerFactory.getLogger(MailingList.class); private static final String IMAP_HOST = "imap_host"; private static final String IMAP_PORT = "imap_port"; @@ -42,14 +48,16 @@ public class MailingList implements MessageHandler { private static final String SMTP_PASS = "smtp_pass"; public static final String TABLE_NAME = "Lists"; private static final int STATE_PENDING = 0; - private static final int STATE_ENABLED = 1; - private static final int STATE_PUBLIC = 2; - public static final int STATE_FORWARD_FROM = 4; - public static final int STATE_FORWARD_ATTACHED = 8; - public static final int STATE_HIDE_RECEIVERS = 16; - public static final int STATE_REPLY_TO_LIST = 32; - public static final int STATE_OPEN = 64; - public static final int STATE_PUBLIC_ARCHIVE = 128; + 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 + public static final int STATE_REPLY_TO_LIST = 32; // set REPLY TO field to list address? + public static final int STATE_OPEN_FOR_GUESTS = 64; // allow anyone to send via this list? + public static final int STATE_PUBLIC_ARCHIVE = 128; // save received messages in archive? + public static final int STATE_OPEN_FOR_SUBSCRIBERS = 256; // allow mods to send via this list? + public static final int STATE_MODS_CAN_EDIT_MODS = 512; // allow mods to make subscribers to mods? private static final int VISIBLE = 1; private static final int HIDDEN = 0; private static final int DEFAULT_STATE = STATE_PENDING|STATE_HIDE_RECEIVERS|STATE_PUBLIC_ARCHIVE; @@ -131,17 +139,6 @@ public class MailingList implements MessageHandler { Database.open().query(sql).compile().run(); } - /** - * load the set of mailing lists a given user is allowed to edit - * @param user - * @return - */ - public static Set editableBy(User user) { - var list = new HashSet(); - for (String key : ListMember.listsOwnedBy(user)) list.add(load(key)); - return list; - } - public String email() { return email; } @@ -212,7 +209,7 @@ public class MailingList implements MessageHandler { try { return members().stream().map(ListMember::user).map(User::email).anyMatch(senderEmail::equals); } catch (SQLException e) { - LOG.warn("hasMember() failded for {}",email(),e); + LOG.warn("hasMember() fehlgeschlagen für {}",email(),e); } return false; } @@ -229,6 +226,20 @@ public class MailingList implements MessageHandler { return setFlag(STATE_HIDE_RECEIVERS,hide); } + public boolean isAllowedSender(String senderEmail){ + if (senderEmail == null || senderEmail.isBlank()) return false; + try { + var user = User.load(senderEmail); + if (user == null) return this.isOpenForGuests(); + var member = ListMember.load(this,user); + if (member == null) return this.isOpenForGuests(); + if (member.isModerator()) return true; + if (member.isSubscriber()) return this.isOpenForSubscribers(); + } catch (SQLException e) { + LOG.warn("Laden des Nutzers zu {} fehlgeschlagen",senderEmail,e); + } + return this.isOpenForGuests(); + } /** * test, whether the current ML is subscribable by a given user * @param user @@ -246,6 +257,14 @@ public class MailingList implements MessageHandler { } } + public boolean isOpenForGuests(){ + return hasState(STATE_OPEN_FOR_GUESTS); + } + + public boolean isOpenForSubscribers(){ + return hasState(STATE_OPEN_FOR_GUESTS|STATE_OPEN_FOR_SUBSCRIBERS); + } + /** * 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. @@ -267,10 +286,32 @@ public class MailingList implements MessageHandler { 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. + * @param listEmails + * @return + */ + public static Set loadAll(Collection listEmails) { + if (listEmails == null) return null; + if (listEmails.isEmpty()) return Set.of(); + var list = new HashSet(); + try { + var rs = Database.open() + .select(TABLE_NAME) + .where(EMAIL,listEmails) + .compile().exec(); + while (rs.next()) list.add(MailingList.from(rs)); + } catch (SQLException e) { + LOG.debug("Konnte Mailing-Listen nicht laden: ",e); + } + return list; + } + public boolean mayBeAlteredBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).isOwner()) return true; + if (ListMember.load(this,user).isModerator()) return true; } catch (SQLException e) { LOG.debug("Fehler beim Laden des Listenmitglieds für ({}, {})",user.email(),email()); } @@ -315,6 +356,28 @@ public class MailingList implements MessageHandler { return map; } + /** + * load the set of mailing lists a given user is allowed to edit + * @param user + * @return + */ + public static List moderatedBy(User user) { + return ListMember.listsOf(user) + .stream() + .filter(listMember -> listMember.isModerator()) + .map(ListMember::list) + .toList(); + + } + + public boolean modsMayEditMods(){ + return hasState(STATE_MODS_CAN_EDIT_MODS); + } + + public MailingList modsMayNominateMods(boolean allowed) throws SQLException { + return setFlag(STATE_MODS_CAN_EDIT_MODS,allowed); + } + public String name(){ return name; } @@ -336,7 +399,7 @@ public class MailingList implements MessageHandler { Address from = message.getFrom()[0]; if (from instanceof InternetAddress internetAddress){ var senderEmail = ((InternetAddress) from).getAddress(); - if (!hasState(STATE_OPEN) && !this.hashMember(senderEmail)){ + if (!isAllowedSender(senderEmail)) { retainMessage(message); sentRetentionNotification(senderEmail); return; @@ -346,8 +409,12 @@ public class MailingList implements MessageHandler { forward(message); } - public MailingList open(boolean open) throws SQLException { - return setFlag(STATE_OPEN,open); + public MailingList openForGuests(boolean open) throws SQLException { + return setFlag(STATE_OPEN_FOR_GUESTS,open); + } + + public MailingList openForSubscribers(boolean open) throws SQLException { + return setFlag(STATE_OPEN_FOR_SUBSCRIBERS,open); } /** @@ -450,7 +517,7 @@ public class MailingList implements MessageHandler { private void sendConfirmationRequest(User user, String token) throws MessagingException, UnsupportedEncodingException { var subject = t("Bitte bestätigen Sie ihr Listen-Abonnement"); var config = Configuration.instance(); - var url = new StringBuilder(config.baseUrl()).append("/confirm?token=").append(token); + var url = new StringBuilder(config.baseUrl()).append("/web/confirm?token=").append(token); var text = t("Botte gehen Sie zu {} um das Abonnieren der Liste abzuschließen!",url); smtp.send(email(),name(),user.email(),subject,text); } @@ -463,13 +530,13 @@ public class MailingList implements MessageHandler { .map(ListMember::user) .map(User::email) .collect(Collectors.joining(", ")); - var subject = t("List '{}' requires attention!",name()); - var text = t("This list received an email from {}, who is not member of the list.\nThe email has been moved to the '{}' folder.\nYou may manually forward this message or drop it.",senderEmail,RETAINED_FOLDER); + var subject = t("Liste '{}' erfordert Aufmerksamkeit!",name()); + var text = t("Diese Liste hat eine E-Mail von {} empfangen. Der Absender ist nicht Mitglied der Liste.\nDie Email wurde in den '{}'-Ordner verschoben.\nSie können die Nachricht manuell weiterleiten oder verwerfen.",senderEmail,RETAINED_FOLDER); smtp.send(email(), name(), receivers,subject,text); - subject = t("Your message to {} was rejected!",email()); - text = t("You have tried to send a message to the list '{}', which failed. This is because you are not a member of this list.\n",name()); - if (hasState(STATE_PUBLIC)) text += t("You may go to {} and subscribe to the list, then try again.",Configuration.instance().baseUrl()); + subject = t("Ihre Nachricht an {} wurde zurückgewiesen!",email()); + text = t("Sie haben versucht, eine Nachricht an die Liste '{}' zu senden. Das wurde verweigert, da Sie nicht Mitglied der Liste sind.\n",name()); + if (hasState(STATE_PUBLIC)) text += t("Sie können zu {} gehen und die Liste abonnieren. Versuchen Sie es danach erneut.",Configuration.instance().baseUrl()); smtp.send(email(), name(), senderEmail,subject,text); } catch (SQLException e){ LOG.error("Failed to load list of owners of mailing list. Retention notification was not sent to owners of {}",email(),e); @@ -492,7 +559,8 @@ public class MailingList implements MessageHandler { 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)) map.put("open",VISIBLE); + 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; } diff --git a/src/main/java/de/srsoftware/widerhall/data/User.java b/src/main/java/de/srsoftware/widerhall/data/User.java index 7473bc1..c66f54d 100644 --- a/src/main/java/de/srsoftware/widerhall/data/User.java +++ b/src/main/java/de/srsoftware/widerhall/data/User.java @@ -266,7 +266,7 @@ public class User { * @return */ public Map safeMap(){ - return Map.of(NAME,name,EMAIL,email,PERMISSIONS,permissionList(),PASSWORD,hashedPassword() == null ? "no" : "yes"); + return Map.of(NAME,name,EMAIL,email,PERMISSIONS,permissionList(),PASSWORD,hashedPassword() == null ? "nein" : "ja"); } /** diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index 7d787a0..a228d78 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -21,16 +21,20 @@ import java.util.Map; import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Util.t; +import static de.srsoftware.widerhall.data.MailingList.*; public class Rest extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(Rest.class); + private static final String LIST_ADD_MOD = "list/add_mod"; private static final String LIST_ARCHIVE = "list/archive"; private static final String LIST_DISABLE = "list/disable"; - private static final String LIST_EDITABLE = "list/editable"; + private static final String LIST_DROP_MEMBER = "list/drop_member"; + private static final String LIST_DROP_MOD = "list/drop_mod"; private static final String LIST_DETAIL = "list/detail"; private static final String LIST_ENABLE = "list/enable"; private static final String LIST_HIDE = "list/hide"; private static final String LIST_MEMBERS = "list/members"; + private static final String LIST_MODERATED = "list/moderated"; private static final String LIST_SHOW = "list/show"; private static final String LIST_TEST = "list/test"; private static final String LIST_SUBSCRIBABLE = "list/subscribable"; @@ -56,6 +60,19 @@ public class Rest extends HttpServlet { return Map.of(SUCCESS,"Nutzer-Berechtigungen aktualisiert"); } + private List archive(HttpServletRequest req) { + var list = Util.getMailingList(req); + if (list != null){ + try { + return Post.find(list).stream().map(Post::safeMap).toList(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + LOG.debug("list: {}",list.email()); + return List.of(); + } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String error = handleGet(req, resp); @@ -105,6 +122,9 @@ public class Rest extends HttpServlet { if (user != null){ json.put(USER,user.safeMap()); switch (path) { + case LIST_ARCHIVE: + 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()); @@ -113,8 +133,8 @@ public class Rest extends HttpServlet { json.put(ERROR,"Laden der Nutzerliste fehlgeschlagen"); } break; - case LIST_EDITABLE: - json.put("lists", MailingList.editableBy(user).stream().map(MailingList::safeMap).toList()); + case LIST_MODERATED: + 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()); @@ -144,18 +164,7 @@ public class Rest extends HttpServlet { } } - private List archive(HttpServletRequest req) { - var list = Util.getMailingList(req); - if (list != null){ - try { - return Post.find(list).stream().map(Post::safeMap).toList(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - LOG.debug("list: {}",list.email()); - return List.of(); - } + public String handlePost(HttpServletRequest req, HttpServletResponse resp){ @@ -170,12 +179,21 @@ public class Rest extends HttpServlet { var userEmail = req.getParameter(EMAIL); var permissions = req.getParameter(PERMISSIONS); switch (path) { + case LIST_ADD_MOD: + json.putAll(listAddMod(list,userEmail,user)); + break; case LIST_DETAIL: json.putAll(listDetail(list,user)); break; case LIST_DISABLE: json.putAll(enableList(list,user,false)); break; + case LIST_DROP_MEMBER: + json.putAll(listDropMember(list,userEmail,user)); + break; + case LIST_DROP_MOD: + json.putAll(listDropMod(list,userEmail,user)); + break; case LIST_ENABLE: json.putAll(enableList(list,user,true)); break; @@ -229,18 +247,65 @@ public class Rest extends HttpServlet { } } + private Map listAddMod(MailingList list, String userEmail, User user) { + ListMember moderator = null; + try { + moderator = ListMember.load(list,user); + } catch (SQLException e) { + 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())); + + var error = moderator.addNewModerator(userEmail); + + return error == null ? Map.of(SUCCESS,t("{} is now a moderator of {}",userEmail,list.email())) : Map.of(ERROR,error); + } + private Map listDetail(MailingList list, User user) { if (list == null) return Map.of(ERROR,"Keine Listen-Email übertragen!"); var map = new HashMap<>(); - if (list.hasState(MailingList.STATE_FORWARD_FROM)) map.put("forward_from",true); - if (list.hasState(MailingList.STATE_FORWARD_ATTACHED)) map.put("forward_attached",true); - if (list.hasState(MailingList.STATE_HIDE_RECEIVERS)) map.put("hide_receivers",true); - if (list.hasState(MailingList.STATE_REPLY_TO_LIST)) map.put("reply_to_list",true); - if (list.hasState(MailingList.STATE_OPEN)) map.put("open",true); - if (list.hasState(MailingList.STATE_PUBLIC_ARCHIVE)) map.put("archive",true); + 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); + if (list.hasState(MailingList.STATE_HIDE_RECEIVERS)) map.put(KEY_HIDE_RECEIVERS,true); + 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.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); return map; } + private Map listDropMember(MailingList list, String userEmail, User user) { + ListMember moderator = null; + try { + moderator = ListMember.load(list,user); + } catch (SQLException e) { + 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())); + + var error = moderator.dropMember(userEmail); + + return error == null ? Map.of(SUCCESS,t("{} is now a moderator of {}",userEmail,list.email())) : Map.of(ERROR,error); + } + + private Map listDropMod(MailingList list, String userEmail, User user) { + ListMember moderator = null; + try { + moderator = ListMember.load(list,user); + } catch (SQLException e) { + 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())); + + var error = moderator.dropModerator(userEmail); + + return error == null ? Map.of(SUCCESS,t("{} is now a moderator of {}",userEmail,list.email())) : Map.of(ERROR,error); + } + private Map listMembers(MailingList list, User user) { if (list == null) return Map.of(ERROR,"Keine Listen-Email übertragen!"); if (!list.membersMayBeListedBy(user)) Map.of(ERROR,t("Es ist dir nicht gestattet, die Mitglieder von '{}' aufzulisten",list.email())); @@ -249,7 +314,7 @@ public class Rest extends HttpServlet { .stream() .map(ListMember::safeMap) .toList(); - return Map.of(MEMBERS,members); + return Map.of(MEMBERS,members,LIST,list.minimalMap()); } catch (SQLException e) { LOG.error("Laden der Mitglieder-Liste fehlgeschlagen: ",e); return Map.of(ERROR,t("Laden der Mitglieder-Liste von '{}' fehlgeschlagen!",list.email())); diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index fb04c7c..e574ce0 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -5,7 +5,6 @@ import de.srsoftware.widerhall.data.ListMember; import de.srsoftware.widerhall.data.MailingList; import de.srsoftware.widerhall.data.Post; import de.srsoftware.widerhall.data.User; -import org.json.simple.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +20,7 @@ import java.util.Map; import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Util.t; +import static de.srsoftware.widerhall.data.MailingList.*; public class Web extends TemplateServlet { public static final String WEB_ROOT = "/web"; @@ -295,17 +295,19 @@ public class Web extends TemplateServlet { if (!error && !list.mayBeAlteredBy(user)) { error = true; - data.put(ERROR,t("Es ist Ihnen nicht gestattet, diese Mailinglist zu verändern!")); + data.put(ERROR,t("Es ist Ihnen nicht gestattet, die Einselltungen dieser Mailingliste zu verändern!")); } if (!error){ try { - list.forwardFrom(Util.getCheckbox(req, "forward_from")) - .forwardAttached(Util.getCheckbox(req, "forward_attached")) - .hideReceivers(Util.getCheckbox(req, "hide_receivers")) - .replyToList(Util.getCheckbox(req, "reply_to_list")) - .open(Util.getCheckbox(req,"open")) - .archive(Util.getCheckbox(req,"archive")); + list.forwardFrom(Util.getCheckbox(req, KEY_FORWARD_FROM)) + .forwardAttached(Util.getCheckbox(req, KEY_FORWARD_ATTACHED)) + .hideReceivers(Util.getCheckbox(req, KEY_HIDE_RECEIVERS)) + .replyToList(Util.getCheckbox(req, KEY_REPLY_TO_LIST)) + .modsMayNominateMods(Util.getCheckbox(req, KEY_MODS_CAN_EDIT_MODS)) + .openForGuests(Util.getCheckbox(req,KEY_OPEN_FOR_GUESTS)) + .openForSubscribers(Util.getCheckbox(req,KEY_OPEN_FOR_SUBSCRIBERS)) + .archive(Util.getCheckbox(req,KEY_ARCHIVE)); data.put(NOTES,t("Mailing-Liste aktualisiert!")); } catch (SQLException e){ LOG.warn("Aktualisierung der Mailing-Liste fehlgeschlagen:",e); @@ -328,7 +330,7 @@ public class Web extends TemplateServlet { return loadTemplate("post",map,resp); } catch (SQLException | IOException e) { LOG.debug("Failed to load post from file!",e); - return t("Failed to load post from file!"); + return t("Laden der Nachricht aus Datei fehlgeschlagen!"); } } @@ -481,10 +483,24 @@ public class Web extends TemplateServlet { return loadTemplate(UNSUBSCRIBE,data,resp); } } - // if we get here, we should have a valid user + + ListMember member = null; + try { + member = ListMember.load(list,user); + } catch (SQLException e) { + LOG.debug("Laden des Listenmitglieds für {}/{} fehlgeschlagen",user.email(),list.email(),e); + data.put(ERROR, t("Laden des Listenmitglieds für {}/{} fehlgeschlagen",user.email(),list.email())); + return loadTemplate(UNSUBSCRIBE,data,resp); + } + if (member == null){ + data.put(ERROR, t("{} ist kein Mitglied von {}",user.email(),list.email())); + return loadTemplate(UNSUBSCRIBE,data,resp); + } + // if we get here, we should have a valid member object try { - ListMember.unsubscribe(list,user); + member.unsubscribe(); data.put(NOTES,t("'{}' erfolgreich abbestellt.",list.email())); + return loadTemplate(INDEX,data,resp); } catch (SQLException e) { LOG.warn("Es ist ein Problem beim Entfernen von {} aus der Liste {} aufgetreten:",user.email(),list.email(),e); diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 38483ff..a57f20a 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -11,6 +11,6 @@ - + diff --git a/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java b/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java new file mode 100644 index 0000000..43f0361 --- /dev/null +++ b/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java @@ -0,0 +1,144 @@ +package de.srsoftware.widerhall.data; + +import junit.framework.TestCase; + +import java.util.Map; + +import static de.srsoftware.widerhall.Constants.*; + +public class ListMemberTest extends TestCase { + + /*public void testConfirm() { + NEEDS MOCK + }*/ + + /*public void testCreate() { + NEEDS MOCK + }*/ + + /*public void testCreateTable() { + NEEDS MOCK + }*/ + + /*public void testFrom() { + NEEDS MOCK + }*/ + + public void testHasState() { + int state = 32 | 8 | 2; + ListMember lm = new ListMember(null,null,state,null); + assertFalse(lm.hasState(1)); + assertTrue(lm.hasState(2)); + assertFalse(lm.hasState(4)); + assertTrue(lm.hasState(8)); + assertFalse(lm.hasState(16)); + assertTrue(lm.hasState(32)); + + assertTrue(lm.hasState(1|2)); + assertFalse(lm.hasState(1|4)); + assertTrue(lm.hasState(1|8)); + assertFalse(lm.hasState(1|16)); + assertTrue(lm.hasState(1|32)); + + assertTrue(lm.hasState(2|4)); + assertTrue(lm.hasState(2|8)); + assertTrue(lm.hasState(2|16)); + assertTrue(lm.hasState(2|32)); + + assertTrue(lm.hasState(4|8)); + assertFalse(lm.hasState(4|16)); + assertTrue(lm.hasState(4|32)); + + assertTrue(lm.hasState(8|16)); + assertTrue(lm.hasState(8|32)); + + assertTrue(lm.hasState(16|32)); + + assertTrue(lm.hasState(1|2|4)); + assertTrue(lm.hasState(1|2|8)); + assertTrue(lm.hasState(1|2|16)); + assertTrue(lm.hasState(1|2|32)); + assertTrue(lm.hasState(1|4|8)); + assertFalse(lm.hasState(1|4|16)); + assertTrue(lm.hasState(1|4|32)); + assertTrue(lm.hasState(1|8|16)); + assertTrue(lm.hasState(1|8|32)); + assertTrue(lm.hasState(1|16|32)); + + assertTrue(lm.hasState(2|4|8)); + assertTrue(lm.hasState(2|4|16)); + assertTrue(lm.hasState(2|4|32)); + assertTrue(lm.hasState(2|8|16)); + assertTrue(lm.hasState(2|8|32)); + assertTrue(lm.hasState(2|16|32)); + + assertTrue(lm.hasState(4|8|16)); + assertTrue(lm.hasState(4|8|32)); + assertTrue(lm.hasState(4|16|32)); + assertTrue(lm.hasState(8|16|32)); + + } + + public void testIsOwner() { + var guest = new ListMember(null,null,ListMember.STATE_SUBSCRIBER,null); + var owner = new ListMember(null,null,ListMember.STATE_OWNER,null); + var mod = new ListMember(null,null,ListMember.STATE_MODERATOR,null); + assertFalse(guest.isOwner()); + assertTrue(owner.isOwner()); + assertFalse(mod.isOwner()); + } + + public void testIsModerator() { + var guest = new ListMember(null,null,ListMember.STATE_SUBSCRIBER,null); + var owner = new ListMember(null,null,ListMember.STATE_OWNER,null); + var mod = new ListMember(null,null,ListMember.STATE_MODERATOR,null); + assertFalse(guest.isModerator()); + assertTrue(owner.isModerator()); + assertTrue(mod.isModerator()); + } + + /*public void testListsOwnedBy() { + NEEDS MOCK + }*/ + + /*public void testLoad() { + NEEDS MOCK + }*/ + + /*public void testOf() { + NEEDS MOCK + }*/ + + public void testSafeMap() { + 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()); + } + + public void testStateText() { + var guest = new ListMember(null,null,ListMember.STATE_SUBSCRIBER,null); + var owner = new ListMember(null,null,ListMember.STATE_OWNER,null); + var mod = new ListMember(null,null,ListMember.STATE_MODERATOR,null); + var await = new ListMember(null,null,ListMember.STATE_AWAITING_CONFIRMATION,null); + + assertEquals("subscriber",guest.stateText()); + assertEquals("moderator, owner, subscriber",owner.stateText()); + assertEquals("moderator, subscriber",mod.stateText()); + assertEquals("awaiting confirmation",await.stateText()); + } + + public void testToken() { + var lm = new ListMember(null,null,0,"bam"); + assertEquals("bam",lm.token()); + } + + /*public void testUnsubscribe() { + NEEDS MOCK + }*/ + + public void testUser() { + var user = new User("email","name","salt","hash",0); + var lm = new ListMember(null,user,ListMember.STATE_AWAITING_CONFIRMATION,"token"); + assertEquals(user,lm.user()); + } +} \ No newline at end of file diff --git a/static/templates/add_list.st b/static/templates/add_list.st index e797d6c..3367570 100644 --- a/static/templates/add_list.st +++ b/static/templates/add_list.st @@ -45,7 +45,7 @@
diff --git a/static/templates/archive.st b/static/templates/archive.st index 4c462b1..d0c5529 100644 --- a/static/templates/archive.st +++ b/static/templates/archive.st @@ -13,9 +13,9 @@

Widerhall List Archive

- - - + + +
DateFromSubjectDatumAbsenderBetreff
«footer()» diff --git a/static/templates/inspect.st b/static/templates/inspect.st index bf8160b..f89555c 100644 --- a/static/templates/inspect.st +++ b/static/templates/inspect.st @@ -11,37 +11,58 @@ «userinfo()» «messages()»

Widerhall '«data.list»'-Details

- «listmembers()»
- Optionen für «data.list» - - - - - - + Einstellungen +
+ Weiterleitungs-Rechte +

+ Wem ist es erlaubt, Nachrichten via «data.list» zu verteilen? +

+ ✓ Besitzer und Moderatoren + + +
+
+ Weiterleitungs-Optionen + + + + +
+
+ andere Optionen + + +
+ «listmembers()» diff --git a/static/templates/js.st b/static/templates/js.st index de98d77..958b87f 100644 --- a/static/templates/js.st +++ b/static/templates/js.st @@ -1,3 +1,7 @@ +function addMod(userEmail,listEmail){ + $.post('/api/list/add_mod',{list:listEmail,email:userEmail},reload,'json'); +} + function addPermission(userEmail,permission){ if (confirm("Wirklich Berechtigung an "+userEmail+" geben?")){ $.post('/api/user/addpermission',{email:userEmail,permissions:permission},reload,'json'); @@ -12,6 +16,15 @@ function dropList(listEmail){ console.log('dopList('+listEmail+')'); } +function dropMember(userEmail,listEmail){ + $.post('/api/list/drop_member',{list:listEmail,email:userEmail},reload,'json'); +} + +function dropMod(userEmail,listEmail){ + $.post('/api/list/drop_mod',{list:listEmail,email:userEmail},reload,'json'); +} + + function dropPermission(userEmail,permission){ if (confirm("Wirklich Berechtigung von "+userEmail+" wegnehmen?")){ $.post('/api/user/droppermission',{email:userEmail,permissions:permission},reload,'json'); @@ -36,8 +49,8 @@ function loadListDetail(listEmail){ $.post('/api/list/detail',{list:listEmail},showListDetail,'json'); } -function loadListOfEditableLists(){ - $.getJSON('/api/list/editable', showListOfEditableLists); +function loadListOfModeratedLists(){ + $.getJSON('/api/list/moderated', showListOfModeratedLists); } function loadListOfSubscribableLists(){ @@ -74,15 +87,14 @@ function showListArchive(data){ } function showListDetail(data){ - if (data.forward_from) $('input[name="forward_from"]').prop('checked',true); - if (data.forward_attached) $('input[name="forward_attached"]').prop('checked',true); - if (data.hide_receivers) $('input[name="hide_receivers"]').prop('checked',true); - if (data.reply_to_list) $('input[name="reply_to_list"]').prop('checked',true); - if (data.open) $('input[name="open"]').prop('checked',true); - if (data.archive) $('input[name="archive"]').prop('checked',true); + var options = ['forward_from','forward_attached','hide_receivers','reply_to_list','open_for_guests','open_for_subscribers','archive','edit_mods']; + options.forEach(function(option,index,array){ + console.log(option,'→',data[option]); + if (data[option]) $('input[name="'+option+'"]').prop('checked',true); + }); } -function showListOfEditableLists(data){ +function showListOfModeratedLists(data){ for (let i in data.lists){ let list = data.lists[i]; let row = $(''); @@ -162,12 +174,20 @@ function showListResult(result){ } function showMembers(data){ + var list_mail = data.list.email.prefix+'@'+data.list.email.domain; for (let i in data.members){ let member = data.members[i]; let row = $(''); $('').text(member.name).appendTo(row); $('').text(member.email).appendTo(row); $('').text(member.state).appendTo(row); + let col = $(''); + console.log("data",data); + if (member.state.includes("moderator")) { + if (!member.state.includes("owner")) $('
\ No newline at end of file diff --git a/static/templates/listmembers.st b/static/templates/listmembers.st index 2a6831b..26688af 100644 --- a/static/templates/listmembers.st +++ b/static/templates/listmembers.st @@ -5,6 +5,7 @@ Name E-Mail Status + Aktionen