diff --git a/doc/data structure.dia b/doc/data structure.dia index e352c42..4a37bce 100644 --- a/doc/data structure.dia +++ b/doc/data structure.dia @@ -1560,5 +1560,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + #State# + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/de/srsoftware/widerhall/Application.java b/src/main/java/de/srsoftware/widerhall/Application.java index 2b00e35..318d9bb 100644 --- a/src/main/java/de/srsoftware/widerhall/Application.java +++ b/src/main/java/de/srsoftware/widerhall/Application.java @@ -43,11 +43,4 @@ public class Application { server.start(); } - - private static void startMailSystem(JSONObject json) { - MessageHandler forward = new Forwarder(json); - new ImapClient(json) - .addListener(forward) - .start(); - } } diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index 7923f90..439c1e1 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -1,5 +1,6 @@ package de.srsoftware.widerhall.data; +import de.srsoftware.widerhall.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,23 +20,27 @@ public class ListMember { public static final String TABLE_NAME = "ListMembers"; public static final int STATE_OWNER = 1; public static final int STATE_SUBSCRIBER = 2; - public static final int STATE_UNCONFIRMED = 4; + public static final int STATE_AWAITING_CONFIRMATION = 4; private static final String LIST_EMAIL = "list_email"; private static final String USER_EMAIL = "user_email"; private static final String STATE = "state"; private static final String TOKEN = "token"; - private final String listEmail; - private final String userEmail; + private final String listEmail,token,userEmail; private final int state; - public ListMember(String listEmail, String userEmail, int state){ + public ListMember(String listEmail, String userEmail, int state, String token){ this.listEmail = listEmail; this.userEmail = userEmail; this.state = state; + this.token = token; } public static ListMember create(MailingList list, User user, int state) throws SQLException { - return new ListMember(list.email(),user.email(),state).save(); + String token = null; + if ((state & STATE_AWAITING_CONFIRMATION) > 0){ + token = Util.sha256(String.join("/",list.email(),user.email(),user.salt())); + } + return new ListMember(list.email(),user.email(),state,token).save(); } public static void createTable() throws SQLException { @@ -94,6 +99,10 @@ public class ListMember { return this; } + public String token() { + return token; + } + public static void unsubscribe(MailingList list, User user) throws SQLException { var db = Database.open(); var rs = db.select(TABLE_NAME) @@ -116,4 +125,5 @@ public class ListMember { } } } + } diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index 08cf8db..4a83954 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -1,9 +1,12 @@ package de.srsoftware.widerhall.data; +import de.srsoftware.widerhall.Configuration; +import de.srsoftware.widerhall.mail.SmtpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.xml.crypto.Data; +import javax.mail.MessagingException; +import java.io.UnsupportedEncodingException; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; @@ -11,6 +14,7 @@ import java.util.List; import java.util.Map; import static de.srsoftware.widerhall.Constants.*; +import static de.srsoftware.widerhall.Util.t; public class MailingList { private static final Logger LOG = LoggerFactory.getLogger(MailingList.class); @@ -22,13 +26,17 @@ public class MailingList { private static final String SMTP_PORT = "smtp_port"; private static final String SMTP_USER = "smtp_user"; private static final String SMTP_PASS = "smtp_pass"; - private static final int ENABLED = 1; - private static final int PUBLIC = 2; + private static final int STATE_PENDING = 0; + private static final int STATE_ENABLED = 1; + private static final int STATE_PUBLIC = 2; private final String name; private final String email; public static final String TABLE_NAME = "Lists"; - private final String imapPass, smtpPass, imapHost, smtpHost, imapUser, smtpUser; - private final int imapPort, smtpPort, state; + private final String imapPass, imapHost, imapUser; + private final int imapPort, state; + private final SmtpClient smtp; + + private static final HashMap lists = new HashMap<>(); public MailingList(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String smtpHost, int smtpPort, String smtpUser, String smtpPass, int state) { this.email = email; @@ -37,15 +45,12 @@ public class MailingList { this.imapPort = imapPort; this.imapUser = imapUser; this.imapPass = imapPass; - this.smtpHost = smtpHost; - this.smtpPort = smtpPort; - this.smtpUser = smtpUser; - this.smtpPass = smtpPass; this.state = state; + this.smtp = new SmtpClient(smtpHost,smtpPort,smtpUser,smtpPass); } public static MailingList create(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String smtpHost, int smtpPort, String smtpUser, String smtpPass) throws SQLException { - return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, smtpHost, smtpPort, smtpUser, smtpPass, ENABLED).save(); + return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, smtpHost, smtpPort, smtpUser, smtpPass, STATE_PENDING).save(); } public static void createTable() throws SQLException { @@ -67,18 +72,28 @@ public class MailingList { Database.open().query(sql).run(); } + public String email() { + return email; + } + + 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 - String expression = enable ? "state = state | "+ENABLED : "state = (~(state & "+ENABLED+"))&(state|"+ENABLED+")"; + String expression = enable ? "state = state | "+ STATE_ENABLED : "state = (~(state & "+ STATE_ENABLED +"))&(state|"+ STATE_ENABLED +")"; Database.open().update(TABLE_NAME,expression).where(EMAIL, listEmail).run(); } 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 - String expression = hide ? "state = (~(state & "+PUBLIC+"))&(state|"+PUBLIC+")" : ("state = state | "+PUBLIC); + String expression = hide ? "state = (~(state & "+ STATE_PUBLIC +"))&(state|"+ STATE_PUBLIC +")" : ("state = state | "+ STATE_PUBLIC); Database.open().update(TABLE_NAME,expression).where(EMAIL, listEmail).run(); } + + public static boolean isOpen(String list) { + return openLists().stream().filter(ml -> ml.email.equals(list)).count() > 0; + } + public static List listsOf(User user) { List keys = (user.is(ADMIN)) ? null : ListMember.listsOwnedBy(user); @@ -108,37 +123,16 @@ public class MailingList { return list; } - public static List openLists() { - var list = new ArrayList(); - try { - var rs = Database.open() - .select(TABLE_NAME,"*", "(" + STATE + " & " + PUBLIC + ") as test") - .where("test", PUBLIC) - .exec(); - while (rs.next()) { - var email = rs.getString(EMAIL); - var name = rs.getString(NAME); - var state = rs.getInt(STATE); - list.add(new MailingList(email, name, null, 0, null, null, null, 0, null, null, state)); - } - } catch (SQLException e) { - LOG.warn("Listing mailing lists failed: ", e); - } - return list; - } - - public static boolean isOpen(String list) { - return openLists().stream().filter(ml -> ml.email.equals(list)).count() > 0; - } public static MailingList load(String listEmail) { - try { + var ml = lists.get(listEmail); + if (ml == null) try { var rs = Database.open() .select(TABLE_NAME) .where(EMAIL,listEmail) .exec(); if (rs.next()){ - return new MailingList(rs.getString(EMAIL), + ml = new MailingList(rs.getString(EMAIL), rs.getString(NAME), rs.getString(IMAP_HOST), rs.getInt(IMAP_PORT), @@ -149,13 +143,38 @@ public class MailingList { rs.getString(SMTP_USER), rs.getString(SMTP_PASS), rs.getInt(STATE)); + lists.put(listEmail,ml); } } catch (SQLException e) { LOG.debug("Failed to load MailingList: ",e); } - return null; + return ml; } + public String name(){ + return name; + } + + public static List openLists() { + var list = new ArrayList(); + try { + var rs = Database.open() + .select(TABLE_NAME,"*", "(" + STATE + " & " + STATE_PUBLIC + ") as test") + .where("test", STATE_PUBLIC) + .exec(); + while (rs.next()) { + var email = rs.getString(EMAIL); + var name = rs.getString(NAME); + var state = rs.getInt(STATE); + list.add(new MailingList(email, name, null, 0, null, null, null, 0, null, null, state)); + } + } catch (SQLException e) { + LOG.warn("Listing mailing lists failed: ", e); + } + return list; + } + + public Map safeMap() { var map = new HashMap(); String[] parts = email.split("@", 2); @@ -164,19 +183,14 @@ public class MailingList { if (imapHost != null) map.put(IMAP_HOST, imapHost); if (imapPort != 0) map.put(IMAP_PORT, imapPort); if (imapUser != null) map.put(IMAP_USER, imapUser); - if (smtpHost != null) map.put(SMTP_HOST, smtpHost); - if (smtpPort != 0) map.put(SMTP_PORT, smtpPort); - if (smtpUser != null) map.put(SMTP_USER, smtpUser); + if (smtp.host() != null) map.put(SMTP_HOST, smtp.host()); + if (smtp.port() != 0) map.put(SMTP_PORT, smtp.port()); + if (smtp.username() != null) map.put(SMTP_USER, smtp.username()); map.put(STATE, stateString(state)); return map; } - private static String stateString(int state) { - var states = new ArrayList(); - states.add((state & ENABLED) == ENABLED ? "enabled" : "disabled"); - states.add((state & PUBLIC) == PUBLIC ? "public" : "hidden"); - return String.join(", ", states); - } + private MailingList save() throws SQLException { Database.open().insertInto(TABLE_NAME) @@ -187,16 +201,44 @@ public class MailingList { Map.entry(IMAP_PORT, imapPort), Map.entry(IMAP_USER, imapUser), Map.entry(IMAP_PASS, imapPass), - Map.entry(SMTP_HOST, smtpHost), - Map.entry(SMTP_PORT, smtpPort), - Map.entry(SMTP_USER, smtpUser), - Map.entry(SMTP_PASS, smtpPass), + Map.entry(SMTP_HOST, smtp.host()), + Map.entry(SMTP_PORT, smtp.port()), + Map.entry(SMTP_USER, smtp.username()), + Map.entry(SMTP_PASS, smtp.password()), Map.entry(STATE, state))) .run(); return this; } - public String email() { - return email; + private void sendConfirmationRequest(User user, String token) throws MessagingException, UnsupportedEncodingException { + var subject = t("Please confirm your list subscription"); + var config = Configuration.instance(); + var url = new StringBuilder(config.baseUrl()).append("/confirm?token=").append(token); + var text = t("Please go to {} in order to complete your list subscription!",url); + smtp.login().send(email(),name(),user.email(),subject,text); + } + + private static String stateString(int state) { + var states = new ArrayList(); + states.add((state & STATE_ENABLED) == STATE_ENABLED ? "enabled" : "disabled"); + states.add((state & STATE_PUBLIC) == STATE_PUBLIC ? "public" : "hidden"); + return String.join(", ", states); + } + + public void requestSubscription(User user) throws SQLException, MessagingException { + var member = ListMember.create(this,user,ListMember.STATE_AWAITING_CONFIRMATION); + var token = member.token(); + try { + sendConfirmationRequest(user, token); + } catch (UnsupportedEncodingException e) { + throw new MessagingException("Failed to send email to "+user.email(),e); + } + } + + + public void test(User user) throws MessagingException, UnsupportedEncodingException { + var subject = t("{}: test mail",name()); + var text = t("If you received this mail, the SMTP settings of your mailing list are correct."); + smtp.login().send(email(),name(),user.email(),subject,text); } } diff --git a/src/main/java/de/srsoftware/widerhall/mail/Forwarder.java b/src/main/java/de/srsoftware/widerhall/mail/Forwarder.java index 9f1c687..0cca288 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/Forwarder.java +++ b/src/main/java/de/srsoftware/widerhall/mail/Forwarder.java @@ -11,22 +11,21 @@ import java.io.UnsupportedEncodingException; public class Forwarder implements MessageHandler { private static final Logger LOG = LoggerFactory.getLogger(Forwarder.class); private final SmtpClient smtp; - private final JSONObject config; + private final String receiver,sender; - public Forwarder(JSONObject config) { - this.config = config; - SmtpClient smtp = new SmtpClient(config); + public Forwarder(String host, int port, String username, String password, String sender, String receiver) { + this.sender = sender; + this.receiver = receiver; + SmtpClient smtp = new SmtpClient(host,port,username,password); this.smtp = smtp; } @Override public void onMessageReceived(Message message) throws MessagingException { LOG.debug("forwarding {}",message.getSubject()); - String testSender = (String) config.get("sender"); - String testReceiver = (String) config.get("receiver"); try { - smtp.send(config,testSender,"Stephan Richter",testReceiver,"Info: "+message.getSubject(),"Neue Mail eingegangen!"); + smtp.send(sender,"Stephan Richter",receiver,"Info: "+message.getSubject(),"Neue Mail eingegangen!"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } diff --git a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java index 3b543a8..526ef22 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java +++ b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java @@ -1,6 +1,5 @@ package de.srsoftware.widerhall.mail; -import org.json.simple.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,7 +8,6 @@ import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import java.io.UnsupportedEncodingException; import java.util.Date; -import java.util.Map; import java.util.Properties; public class SmtpClient { @@ -19,23 +17,33 @@ public class SmtpClient { private static final String AUTH = "mail.smtp.auth"; private static final String SSL = "mail.smtp.ssl.enable"; private static final String UTF8 = "UTF-8"; + private final String host,password,username; + private final int port; private Session session; - public SmtpClient(Map config){ - String host = (String) config.get("host"); - long port = (long) config.get("port"); - Properties props = new Properties(); - props.put(HOST,host); - props.put(PORT,port); - props.put(AUTH,true); - props.put(SSL,true); - - session = Session.getInstance(props); - LOG.debug("Created new {}: {}", getClass().getSimpleName(),session); + public SmtpClient(String host, int port, String username, String password){ + this.username = username; + this.password = password; + this.host = host; + this.port = port; } - public void send(JSONObject config, String senderAdress, String senderName, String receivers, String subject, String content) throws MessagingException, UnsupportedEncodingException { + public SmtpClient login(){ + if (session == null) { + Properties props = new Properties(); + props.put(HOST, host); + props.put(PORT, port); + props.put(AUTH, true); + props.put(SSL, true); + + session = Session.getInstance(props); + LOG.debug("Created new session: {}", session); + } + return this; + } + + public void send(String senderAdress, String senderName, String receivers, String subject, String content) throws MessagingException, UnsupportedEncodingException { MimeMessage message = new MimeMessage(session); message.addHeader("Content-Type","text/plain; charset="+UTF8); message.addHeader("format","flowed"); @@ -48,12 +56,24 @@ public class SmtpClient { message.setSentDate(new Date()); message.setRecipients(Message.RecipientType.TO,InternetAddress.parse(receivers,false)); - String username = (String) config.get("user"); - String password = (String) config.get("password"); - 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; + } } diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index b4e5754..54363b2 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -7,12 +7,14 @@ import org.json.simple.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.mail.MessagingException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Serializable; +import java.io.UnsupportedEncodingException; import java.sql.SQLException; import java.util.List; import java.util.Map; @@ -29,8 +31,10 @@ public class Rest extends HttpServlet { private static final String LIST_HIDE = "list/hide"; private static final String LIST_MEMBERS = "list/members"; private static final String LIST_SHOW = "list/show"; + private static final String LIST_TEST = "list/test"; private static final String USER_LIST = "user/list"; private static final String MEMBERS = "members"; + private static final String SUCCESS = "success"; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -113,6 +117,9 @@ public class Rest extends HttpServlet { case LIST_SHOW: json.putAll(hideList(listEmail,user,false)); break; + case LIST_TEST: + json.putAll(testList(listEmail,user)); + break; default: json.put(ERROR,t("No handler for path '{}'!",path)); break; @@ -154,7 +161,7 @@ public class Rest extends HttpServlet { if (user.is(ADMIN) || ListMember.listsOwnedBy(user).contains(listEmail)){ try { MailingList.enable(listEmail,enable); - return Map.of("success",t("Mailing list '{}' was {}!",listEmail,enable ? "enabled" : "disabled")); + return Map.of(SUCCESS,t("Mailing list '{}' was {}!",listEmail,enable ? "enabled" : "disabled")); } catch (SQLException e) { LOG.error("Failed to enable/disable mailing list: ",e); return Map.of("error",t("Failed to update list '{}'",listEmail)); @@ -167,15 +174,23 @@ public class Rest extends HttpServlet { if (user.is(ADMIN) || ListMember.listsOwnedBy(user).contains(listEmail)){ try { MailingList.hide(listEmail,hide); - return Map.of("success",t("Mailing list '{}' was {}!",listEmail,hide ? "hidden" : "made public")); + return Map.of(SUCCESS,t("Mailing list '{}' was {}!",listEmail,hide ? "hidden" : "made public")); } catch (SQLException e) { LOG.error("Failed to (un)hide mailing list: ",e); return Map.of("error",t("Failed to update list '{}'",listEmail)); } - } else { - return Map.of("error",t("You are not allowed to edit '{}'",listEmail)); } + return Map.of("error",t("You are not allowed to edit '{}'",listEmail)); + } + private Map testList(String listEmail, User user) { + try { + MailingList.load(listEmail).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())); + } } } diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 8e51aea..8934b71 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import org.stringtemplate.v4.STGroup; import org.stringtemplate.v4.STRawGroupDir; +import javax.mail.MessagingException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -360,7 +361,7 @@ public class Web extends HttpServlet { data.put(USER,user.safeMap()); try { - ListMember.create(list,user,ListMember.STATE_SUBSCRIBER); + list.requestSubscription(user); data.put(NOTES,t("Successfully subscribed '{}' to '{}'.",user.email(),list.email())); return loadTemplate(INDEX,data,resp); } catch (SQLException sqle) { @@ -371,6 +372,10 @@ public class Web extends HttpServlet { data.put(ERROR,t("You already are member of this list!",sqle.getMessage())); } else data.put(ERROR,t("Subscription failed: {}",sqle.getMessage())); return loadTemplate(SUBSCRIBE,data,resp); + } catch (MessagingException e) { + LOG.warn("Failed to send request confirmation email:",e); + data.put(ERROR,t("Failed to send request confirmation email: {}",e.getMessage())); + return loadTemplate(SUBSCRIBE,data,resp); } } diff --git a/static/templates/js.st b/static/templates/js.st index 1da2843..0c034fd 100644 --- a/static/templates/js.st +++ b/static/templates/js.st @@ -58,7 +58,7 @@ function showListAdminList(data){ if (confirm("This will "+action+" '"+list+"'. Are you sure?"))self[action+'List'](list); }); $('