diff --git a/doc/data structure.dia b/doc/data structure.dia index e887b44..97d23ed 100644 --- a/doc/data structure.dia +++ b/doc/data structure.dia @@ -1051,13 +1051,13 @@ - + - + @@ -1080,7 +1080,7 @@ - + @@ -1096,13 +1096,13 @@ - + - + @@ -1125,7 +1125,7 @@ - + @@ -1141,13 +1141,13 @@ - + - + @@ -1161,7 +1161,7 @@ - #From# + #FromAddr# @@ -1170,7 +1170,7 @@ - + @@ -1186,13 +1186,13 @@ - + - + @@ -1215,7 +1215,7 @@ - + @@ -1228,13 +1228,13 @@ - + - + @@ -1267,16 +1267,16 @@ - + - + - + - + @@ -1299,7 +1299,7 @@ - + @@ -1312,16 +1312,16 @@ - + - + - + - + @@ -1344,7 +1344,7 @@ - + @@ -1357,16 +1357,16 @@ - + - + - + - + @@ -1389,7 +1389,7 @@ - + @@ -1402,16 +1402,16 @@ - + - + - + - + @@ -1425,7 +1425,7 @@ - #Content# + #File# @@ -1434,7 +1434,7 @@ - + @@ -1447,16 +1447,16 @@ - + - + - - + + - + @@ -1482,13 +1482,13 @@ - + - + - + @@ -1695,5 +1695,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + #FromAddr# + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index c4f367f..88a8a0f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,11 @@ org.example Widerhall - 0.0.22 +<<<<<<< HEAD + 0.0.23 +======= + 0.2.1 +>>>>>>> main diff --git a/src/main/java/de/srsoftware/widerhall/Application.java b/src/main/java/de/srsoftware/widerhall/Application.java index 59a5454..79ef706 100644 --- a/src/main/java/de/srsoftware/widerhall/Application.java +++ b/src/main/java/de/srsoftware/widerhall/Application.java @@ -1,5 +1,6 @@ package de.srsoftware.widerhall; +import de.srsoftware.widerhall.data.MailingList; import de.srsoftware.widerhall.web.Front; import de.srsoftware.widerhall.web.Rest; import de.srsoftware.widerhall.web.Web; @@ -20,8 +21,13 @@ public class Application { while (!config.configFile().equals(config.file())) config.load(config.configFile()); if (!config.configFile().exists()) config.save(); - //startMailSystem(json); + startWebserver(); + startMailsystem(); + } + + private static void startMailsystem() { + MailingList.startEnabled(); } private static void startWebserver() throws Exception { diff --git a/src/main/java/de/srsoftware/widerhall/Constants.java b/src/main/java/de/srsoftware/widerhall/Constants.java index 22ea782..b84620f 100644 --- a/src/main/java/de/srsoftware/widerhall/Constants.java +++ b/src/main/java/de/srsoftware/widerhall/Constants.java @@ -12,6 +12,7 @@ public class Constants { public static final String IMAPS = "imaps"; public static final String INBOX = "inbox"; public static final String INDEX = "index"; + public static final String ID = "id"; public static final String INT = "INT"; public static final String LIST = "list"; public static final String LOCATIONS = "locations"; @@ -23,6 +24,8 @@ public class Constants { public static final String PREFIX = "prefix"; public static final String PROTOCOL = "mail.store.protocol"; public static final String STATE = "state"; + public static final String SUBJECT = "subject"; + public static final String TEXT = "text"; public static final String TOKEN = "token"; public static final String USER = "user"; public static final String VARCHAR = "VARCHAR(255)"; diff --git a/src/main/java/de/srsoftware/widerhall/Util.java b/src/main/java/de/srsoftware/widerhall/Util.java index 8dbee4e..3f64f67 100644 --- a/src/main/java/de/srsoftware/widerhall/Util.java +++ b/src/main/java/de/srsoftware/widerhall/Util.java @@ -4,9 +4,14 @@ 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; @@ -118,4 +123,43 @@ public class Util { 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 null; + } + + public static String dropEmail(String tx) { + return tx.replaceAll( "[.\\-\\w]+@[.\\-\\w]+", "[email_removed]"); + } } diff --git a/src/main/java/de/srsoftware/widerhall/data/Database.java b/src/main/java/de/srsoftware/widerhall/data/Database.java index cfcfa2d..41ab4ed 100644 --- a/src/main/java/de/srsoftware/widerhall/data/Database.java +++ b/src/main/java/de/srsoftware/widerhall/data/Database.java @@ -236,7 +236,7 @@ public class Database { /** * add a where condition in the form of … WHERE [key] in ([value]) * @param key - * @param values + * @param value * @return */ private Request where(String key, Object value) { @@ -260,6 +260,7 @@ public class Database { if (!tableExists(User.TABLE_NAME)) User.createTable(); if (!tableExists(MailingList.TABLE_NAME)) MailingList.createTable(); if (!tableExists(ListMember.TABLE_NAME)) ListMember.createTable(); + if (!tableExists(Post.TABLE_NAME)) Post.createTable(); return this; } diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index 7b69b0b..299687f 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -136,6 +136,10 @@ public class ListMember { } + public boolean isOwner(){ + return hasState(STATE_OWNER); + } + /** * return a set of list emails of MailingLists owned by the given user * @param user diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index 77c63f5..4362357 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -1,20 +1,26 @@ 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; +import javax.mail.Address; +import javax.mail.Flags; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; -import java.io.UnsupportedEncodingException; +import javax.ws.rs.HEAD; +import java.io.*; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import static de.srsoftware.widerhall.Constants.*; @@ -40,8 +46,14 @@ public class MailingList implements MessageHandler { 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 VISIBLE = 1; private static final int HIDDEN = 0; + private static final int DEFAULT_STATE = STATE_PENDING|STATE_HIDE_RECEIVERS|STATE_PUBLIC_ARCHIVE; + private static final String RETAINED_FOLDER = "retained"; private final String name; private final String email; private int state; @@ -72,6 +84,10 @@ public class MailingList implements MessageHandler { this.imap = new ImapClient(imapHost,imapPort,imapUser,imapPass,inbox); } + public MailingList archive(boolean enabled) throws SQLException { + return setFlag(STATE_PUBLIC_ARCHIVE,enabled); + } + /** * create a new ML object int the database * @param email @@ -88,7 +104,7 @@ public class MailingList implements MessageHandler { * @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) throws SQLException { - return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, STATE_PENDING).save(); + return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, DEFAULT_STATE).save(); } /** @@ -131,32 +147,37 @@ public class MailingList implements MessageHandler { } - public void enable(boolean enable) throws SQLException { + public MailingList enable(boolean enable) throws SQLException { + if (!enable) imap.stop(); setFlag(STATE_ENABLED,enable); - - if (enable) { - imap.start().addListener(this); - } else { - imap.stop(); - } + if (enable) imap.start().addListener(this); + return this; } private void forward(Message message) throws MessagingException { try { - var emails = members().stream().map(ListMember::user).map(User::email).toList(); - String sender = (state & STATE_FORWARD_FROM) > 0 ? message.getFrom()[0].toString() : email(); - smtp.bccForward(sender,message,emails); + String newSender = !hasState(STATE_FORWARD_FROM) ? email() : null; + var receivers = members() + .stream() + .map(ListMember::user) + .map(User::email) + .toList(); + var subject = message.getSubject(); + + if (!subject.contains(stamp())) subject = stamp()+" "+subject; + var replyTo = (newSender == null && hasState(STATE_REPLY_TO_LIST)) ? email() : null; + smtp.forward(newSender,receivers,message,subject,hasState(STATE_FORWARD_ATTACHED),hasState(STATE_HIDE_RECEIVERS),replyTo); } catch (SQLException e) { LOG.error("Laden der Listen-Mitglieder von {} fehlgeschlagen. Nachricht kann nicht weitergeleitet werden!",email(),e); } } - public void forwardAttached(boolean forward) throws SQLException { - setFlag(STATE_FORWARD_ATTACHED,forward); + public MailingList forwardAttached(boolean forward) throws SQLException { + return setFlag(STATE_FORWARD_ATTACHED,forward); } - public void forwardFrom(boolean forward) throws SQLException { - setFlag(STATE_FORWARD_FROM,forward); + public MailingList forwardFrom(boolean forward) throws SQLException { + return setFlag(STATE_FORWARD_FROM,forward); } @@ -186,12 +207,26 @@ public class MailingList implements MessageHandler { return ml; } + private boolean hashMember(String senderEmail) { + if (senderEmail == null) return false; + try { + return members().stream().map(ListMember::user).map(User::email).anyMatch(senderEmail::equals); + } catch (SQLException e) { + LOG.warn("hasMember() failded for {}",email(),e); + } + return false; + } + public boolean hasState(int test){ return (state & test) > 0; } - public void hide(boolean hide) throws SQLException { - setFlag(STATE_PUBLIC,!hide); + public MailingList hide(boolean hide) throws SQLException { + return setFlag(STATE_PUBLIC,!hide); + } + + public MailingList hideReceivers(boolean hide) throws SQLException { + return setFlag(STATE_HIDE_RECEIVERS,hide); } /** @@ -200,7 +235,7 @@ public class MailingList implements MessageHandler { * @return */ public boolean isOpenFor(User user) { - if ((state & STATE_PUBLIC) > 0) return true; // all users may subscribe public mailing lists + if (hasState(STATE_PUBLIC)) return true; // all users may subscribe public mailing lists if (user == null) return false; try { var member = ListMember.load(this,user); @@ -235,7 +270,7 @@ public class MailingList implements MessageHandler { public boolean mayBeAlteredBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + if (ListMember.load(this,user).isOwner()) return true; } catch (SQLException e) { LOG.debug("Fehler beim Laden des Listenmitglieds für ({}, {})",user.email(),email()); } @@ -245,7 +280,7 @@ public class MailingList implements MessageHandler { public boolean mayBeTestedBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + if (ListMember.load(this,user).isOwner()) return true; } catch (SQLException e) { LOG.debug("Fehler beim Laden des Listenmitglieds für ({}, {})",user.email(),email()); } @@ -259,7 +294,7 @@ public class MailingList implements MessageHandler { public boolean membersMayBeListedBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + if (ListMember.load(this,user).isOwner()) return true; } catch (SQLException e) { LOG.debug("Fehler beim Laden des Listenmitglieds für ({}, {})",user.email(),email()); } @@ -287,10 +322,34 @@ public class MailingList implements MessageHandler { @Override public void onMessageReceived(Message message) throws MessagingException { LOG.debug("Nachricht empfangen: {}",message.getFrom()); - storeMessage(message); + String subject = message.getSubject(); + if (subject.toLowerCase().contains("undelivered")){ + try { + var receivers = members().stream().filter(ListMember::isOwner).map(ListMember::user).map(User::email).toList(); + smtp.forward(email(), receivers, message, message.getSubject(), false,false,null); + } catch (SQLException e){ + LOG.error("Was not able to load members of {}; Non-Delivery notification dropped!",this.email(),e); + } + return; + } + + Address from = message.getFrom()[0]; + if (from instanceof InternetAddress internetAddress){ + var senderEmail = ((InternetAddress) from).getAddress(); + if (!hasState(STATE_OPEN) && !this.hashMember(senderEmail)){ + retainMessage(message); + sentRetentionNotification(senderEmail); + return; + } + } + if (hasState(STATE_PUBLIC_ARCHIVE)) storeMessage(message); forward(message); } + public MailingList open(boolean open) throws SQLException { + return setFlag(STATE_OPEN,open); + } + /** * provide the set of mailing lists that are publicy open to subscriptions * @return @@ -311,7 +370,33 @@ public class MailingList implements MessageHandler { return list; } + public MailingList replyToList(boolean on) throws SQLException { + return setFlag(STATE_REPLY_TO_LIST,on); + } + + private void retainMessage(Message message) { + String subject = "unknown mail"; + try { + subject = message.getSubject(); + imap.move(message, RETAINED_FOLDER); + return; + } catch (MessagingException e){ + LOG.warn("Retaining message {} failed!",subject,e); + } + try { + message.setFlag(Flags.Flag.SEEN, true); + return; + } catch (MessagingException e) { + LOG.warn("Failed to flag message {} as SEEN!",subject,e); + } + try { + LOG.error("Retaining message {} failed. To avoid dead loop, the MailingList '{}' will be stopped!",subject,email()); + enable(false); + } catch (SQLException sqle) { + LOG.debug("Failed to update list state in database:",sqle); + } + } /** * creates a map of the current ML containing all fields but passwords. * @return @@ -367,12 +452,36 @@ public class MailingList implements MessageHandler { var config = Configuration.instance(); var url = new StringBuilder(config.baseUrl()).append("/confirm?token=").append(token); var text = t("Botte gehen Sie zu {} um das Abonnieren der Liste abzuschließen!",url); - smtp.login().send(email(),name(),user.email(),subject,text); + smtp.send(email(),name(),user.email(),subject,text); + } + + private void sentRetentionNotification(String senderEmail) { + try { + var receivers = members() + .stream() + .filter(ListMember::isOwner) + .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); + 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()); + 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); + } catch (MessagingException | UnsupportedEncodingException e){ + LOG.error("Failed to send retention notification to owners of {}",email(),e); + } } - private void setFlag(int flag, boolean on) throws SQLException { + private MailingList setFlag(int flag, boolean on) throws SQLException { state = on ? state | flag : state ^ (state & flag); Database.open().update(TABLE_NAME).set(STATE,state).where(EMAIL, email()).compile().run(); + return this; } public Map stateMap(){ @@ -380,6 +489,11 @@ public class MailingList implements MessageHandler { 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)) map.put("open",VISIBLE); + if (hasState(STATE_PUBLIC_ARCHIVE)) map.put("archive",VISIBLE); return map; } @@ -441,8 +555,25 @@ public class MailingList implements MessageHandler { } } - private void storeMessage(Message message){ - // TODO: implement + + private String stamp() { + return "["+name+"]"; + } + + public static void startEnabled() { + try { + var rs = Database.open().select(TABLE_NAME).compile().exec(); + while (rs.next()) { + var list = MailingList.from(rs); + if (list.hasState(STATE_ENABLED)) list.enable(true); + } + } catch (SQLException e) { + LOG.debug("Failed to load MailingLists."); + } + } + + private void storeMessage(Message message) { + Post.create(this,message); } diff --git a/src/main/java/de/srsoftware/widerhall/data/Post.java b/src/main/java/de/srsoftware/widerhall/data/Post.java new file mode 100644 index 0000000..75eeeda --- /dev/null +++ b/src/main/java/de/srsoftware/widerhall/data/Post.java @@ -0,0 +1,169 @@ +package de.srsoftware.widerhall.data; + +import de.srsoftware.widerhall.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static de.srsoftware.widerhall.Constants.*; +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 FROM_ADDR = "from_addr"; + private static final String FROM_NAME = "from_name"; + 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 cache = new HashMap<>(); + + private String id, fromAddr, fromName, subject, filename; + private MailingList list; + private Post parent; + private Long timestamp; + + public Post(String id, MailingList list, String fromAddr, String fromName, String subject, Long timestamp){ + this.id = id; + this.list = list; + this.fromAddr = fromAddr; + this.fromName = fromName; + this.subject = subject; + this.timestamp = timestamp; + this.filename = generateFilename(); + } + + + public static Post create(MailingList list, Message message){ + try { + var id = message.getHeader("Message-ID")[0].replace("<", "").replace(">", ""); + var addr = ((InternetAddress) message.getFrom()[0]); + var fromEmail = addr.getAddress(); + var fromName = addr.getPersonal(); + if (fromName == null || fromName.isBlank()) fromName = fromEmail.split("@")[0] + "@xxxxxx"; + var subject = message.getSubject(); + var text = Util.getText(message); + var time = message.getSentDate().getTime(); + + Post post = new Post(id,list,fromEmail,fromName,subject,time); + Files.writeString(post.file().toPath(),text, StandardCharsets.UTF_8); + return post.save(); + } catch (MessagingException | IOException | SQLException e) { + LOG.warn("Failed to create post from {}",message); + } + return null; + } + + /** + * create posts table + * @throws SQLException + */ + public static void createTable() throws SQLException { + var sql = new StringBuilder() + .append("CREATE TABLE ").append(TABLE_NAME) + .append(" (") + .append(ID).append(" ").append(VARCHAR).append(" NOT NULL PRIMARY KEY, ") + .append(LIST).append(" ").append(VARCHAR).append(", ") + .append(FROM_ADDR).append(" ").append(VARCHAR).append(", ") + .append(FROM_NAME).append(" ").append(VARCHAR).append(", ") + .append(PARENT).append(" ").append(VARCHAR).append(", ") + .append(SUBJECT).append(" ").append(VARCHAR).append(", ") + .append(DATE).append(" ").append(LONG).append(", ") + .append(FILE).append(" ").append(VARCHAR) + + .append(");"); + Database.open().query(sql).compile().run(); + } + + + public File file(){ + return new File(filename); + } + + public static HashSet find(MailingList list) throws SQLException { + var rs = Database.open().select(TABLE_NAME).where(LIST,list.email()).compile().exec(); + try { + var result = new HashSet(); + 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); + var post = cache.get(id); + if (post == null) { + var list = MailingList.load(rs.getString(LIST)); + post = new Post(id, list, rs.getString(FROM_ADDR), rs.getString(FROM_NAME), rs.getString(SUBJECT), rs.getLong(DATE)); + cache.put(id,post); + } + return post; + } catch (SQLException e){ + LOG.debug("Failed to load Post from database!",e); + } + return null; + } + + + private String generateFilename() { + return "/tmp/"+id+".json"; + } + + public String id() { + return id; + } + + public static Post load(String id) throws SQLException { + var rs = Database.open().select(TABLE_NAME).where(ID,id).compile().exec(); + try { + if (rs.next()) return Post.from(rs); + } finally { + rs.close(); + } + return null; + } + + public Map map() { + return Map.of(ID,id, + LIST,list.email(), + FROM_ADDR,fromAddr, + FROM_NAME,fromName, + SUBJECT,subject, + DATE,timestamp, + FILE,filename); + } + + public Map safeMap() { + return Map.of(ID,id, + LIST,list.name(), + FROM_NAME,fromName, + SUBJECT,Util.dropEmail(subject), + DATE,timestamp); + } + + private Post save() throws SQLException { + Database.open().insertInto(TABLE_NAME).values(map()).compile().run(); + return this; + } + + public long timestamp(){ + return timestamp; + } + +} diff --git a/src/main/java/de/srsoftware/widerhall/mail/ImapClient.java b/src/main/java/de/srsoftware/widerhall/mail/ImapClient.java index f87d31f..a3a85eb 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/ImapClient.java +++ b/src/main/java/de/srsoftware/widerhall/mail/ImapClient.java @@ -23,6 +23,7 @@ public class ImapClient { private static final Logger LOG = LoggerFactory.getLogger(ListeningThread.class); private HashSet listeners = new HashSet<>(); private boolean stopped = false; + private Session session; public ListeningThread addListener(MessageHandler messageHandler) { listeners.add(messageHandler); @@ -53,7 +54,7 @@ public class ImapClient { private void openInbox() throws MessagingException { LOG.debug("Verbinden und Einloggen…"); Properties props = imapProps(); - Session session = Session.getInstance(props); + session = Session.getInstance(props); Store store = session.getStore(Constants.IMAPS); store.connect(host,username,password); LOG.debug("Verbunden. Öffne {}:",folderName); @@ -75,6 +76,8 @@ public class ImapClient { 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); } } @@ -158,6 +161,21 @@ public class ImapClient { 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(); + if (!source.isOpen()) source.open(Folder.READ_WRITE); + var messages = new Message[]{message}; + var store = source.getStore(); + var dest = store.getFolder(new URLName(destinationFolder)); + if (!dest.exists()) dest.create(Folder.HOLDS_MESSAGES); + source.copyMessages(messages,dest); + source.setFlags(messages, new Flags(Flags.Flag.DELETED), true); + source.close(true); + return this; + } + + public ImapClient start() { stop(); diff --git a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java index 9044996..0922005 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java +++ b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java @@ -30,21 +30,49 @@ public class SmtpClient { this.port = port; } - public void bccForward(String from, Message message, List emails) throws MessagingException { + + + public void forward(String newSender, List receivers, Message message, String subject, boolean forwardAsAttachment, boolean bcc, String replyTo) throws MessagingException { if (session == null) login(); MimeMessage forward = new MimeMessage(session); - forward.setFrom(from); - forward.setRecipients(Message.RecipientType.BCC,InternetAddress.parse(String.join(", ",emails))); - forward.setSubject(message.getSubject()); + var oldSender = message.getFrom()[0].toString(); + if (newSender != null){ + var pos = subject.indexOf(" (from "); + while (pos > 0){ + var end = subject.indexOf(')',pos); + if (end < pos) break; + subject = (subject.substring(0,pos)+subject.substring(end+1)).trim(); + pos = subject.indexOf(" (from "); + } + forward.setFrom(newSender); + forward.setSubject(subject+" (from "+oldSender+")"); + } else { + forward.setFrom(oldSender); + forward.setSubject(subject); + } + if (replyTo != null) forward.setReplyTo(InternetAddress.parse(replyTo)); + var recipientType = bcc ? Message.RecipientType.BCC : Message.RecipientType.TO; + forward.setRecipients(recipientType,InternetAddress.parse(String.join(", ",receivers))); MimeMultipart multipart = new MimeMultipart(); - MimeBodyPart messageBodyPart = new MimeBodyPart(); - - messageBodyPart.setDataHandler(message.getDataHandler()); - multipart.addBodyPart(messageBodyPart); - + if (forwardAsAttachment){ + MimeBodyPart bodyPart = new MimeBodyPart(); + 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 + bodyPart = new MimeBodyPart(); + // forwardedMsg is the MimeMessage object you want to forward as an attachment + bodyPart.setContent(message, "message/rfc822"); + bodyPart.setDisposition(Part.ATTACHMENT); + multipart.addBodyPart(bodyPart); + } else { + MimeBodyPart bodyPart = new MimeBodyPart(); + + bodyPart.setDataHandler(message.getDataHandler()); + multipart.addBodyPart(bodyPart); + } forward.setContent(multipart); - send(forward); } @@ -64,6 +92,7 @@ public class SmtpClient { } public void send(String senderAdress, String senderName, String receivers, String subject, String content) throws MessagingException, UnsupportedEncodingException { + login(); MimeMessage message = new MimeMessage(session); message.addHeader("Content-Type","text/plain; charset="+UTF8); message.addHeader("format","flowed"); diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index 7cbbb4c..0d1a354 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -3,6 +3,7 @@ package de.srsoftware.widerhall.web; import de.srsoftware.widerhall.Util; 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; @@ -27,6 +28,7 @@ import static de.srsoftware.widerhall.Util.t; public class Rest extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(Rest.class); + 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_DETAIL = "list/detail"; @@ -127,6 +129,9 @@ public class Rest extends HttpServlet { } } else { switch (path) { + case LIST_ARCHIVE: + json.put("archive",archive(req)); + break; case LIST_SUBSCRIBABLE: json.put("lists", MailingList.subscribable().stream().map(MailingList::minimalMap).toList()); break; @@ -143,6 +148,19 @@ public class Rest extends HttpServlet { } } + private Map archive(HttpServletRequest req) { + var list = Util.getMailingList(req); + if (list != null){ + try { + return Post.find(list).stream().collect(Collectors.toMap(Post::timestamp,Post::safeMap)); + } catch (SQLException e) { + e.printStackTrace(); + } + } + LOG.debug("list: {}",list.email()); + return Map.of(); + } + public String handlePost(HttpServletRequest req, HttpServletResponse resp){ var user = Util.getUser(req); @@ -220,6 +238,10 @@ public class Rest extends HttpServlet { 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); return map; } diff --git a/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java b/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java index 4a9081b..69b8487 100644 --- a/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java +++ b/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java @@ -8,6 +8,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Map; @@ -40,6 +41,7 @@ public abstract class TemplateServlet extends HttpServlet { var template = templates.getInstanceOf(path); if (template != null){ try { + resp.setCharacterEncoding("UTF-8"); template.add("data",data); resp.getWriter().println(template.render()); return null; diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 6418be8..dd6886a 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -3,6 +3,7 @@ package de.srsoftware.widerhall.web; import de.srsoftware.widerhall.Util; 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; @@ -12,6 +13,7 @@ import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.nio.file.Files; import java.security.InvalidKeyException; import java.sql.SQLException; import java.util.HashMap; @@ -26,9 +28,11 @@ public class Web extends TemplateServlet { private static final String CONFIRM = "confirm"; private static final Logger LOG = LoggerFactory.getLogger(Web.class); private static final String ADMIN = "admin"; + private static final String ARCHIVE = "archive"; private static final String INSPECT = "inspect"; private static final String LOGIN = "login"; private static final String LOGOUT = "logout"; + private static final String POST = "post"; private static final String REGISTER = "register"; private static final String RELOAD = "reload"; private static final String SUBSCRIBE = "subscribe"; @@ -130,6 +134,12 @@ public class Web extends TemplateServlet { } } + private String archive(HttpServletRequest req, HttpServletResponse resp) { + var domain = req.getParameter(DOMAIN); + var prefix = req.getParameter(PREFIX); + return loadTemplate(ARCHIVE,Map.of(DOMAIN,domain,PREFIX,prefix),resp); + } + private String confirm(HttpServletRequest req, HttpServletResponse resp) { try { var token = req.getParameter(TOKEN); @@ -181,8 +191,12 @@ public class Web extends TemplateServlet { var list = MailingList.load(listEmail); if (list != null) data.put(LIST,list.minimalMap()); switch (path){ + case ARCHIVE: + return archive(req,resp); case CONFIRM: return confirm(req,resp); + case POST: + return post(req,resp); case RELOAD: loadTemplates(); data.put(NOTES,t("Vorlagen wurden neu geladen")); @@ -223,8 +237,6 @@ public class Web extends TemplateServlet { return redirectTo(LOGIN,resp); } - - private String handleLogin(HttpServletRequest req, HttpServletResponse resp) { var email = req.getParameter("email"); var pass = req.getParameter("pass"); @@ -288,21 +300,38 @@ public class Web extends TemplateServlet { } if (!error){ - var dummy = req.getParameterMap(); try { - list.forwardFrom(Util.getCheckbox(req, "forward_from")); - list.forwardAttached(Util.getCheckbox(req, "forward_attached")); + 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")); data.put(NOTES,t("Mailing-Liste aktualisiert!")); } catch (SQLException e){ LOG.warn("Aktualisierung der Mailing-Liste fehlgeschlagen:",e); data.put(ERROR,t("Aktualisierung der Mailing-Liste fehlgeschlagen!")); } - LOG.debug("params: {}",dummy); } return loadTemplate(INSPECT,data,resp); } + private String post(HttpServletRequest req, HttpServletResponse resp) { + var id = req.getParameter(ID); + if (id == null) return t("Could not find email with id!"); + try { + var post = Post.load(id); + var map = new HashMap(); + map.putAll(post.safeMap()); + String content = Files.readString(post.file().toPath()); + map.put("text",Util.dropEmail(content)); + 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!"); + } + } private String redirectTo(String page, HttpServletResponse resp) { try { diff --git a/static/templates/archive.st b/static/templates/archive.st new file mode 100644 index 0000000..4c462b1 --- /dev/null +++ b/static/templates/archive.st @@ -0,0 +1,26 @@ + + + + + + + + + + «navigation()» + «userinfo()» + «messages()» +

Widerhall List Archive

+ + + + + + +
DateFromSubject
+ «footer()» + + + \ No newline at end of file diff --git a/static/templates/inspect.st b/static/templates/inspect.st index a883713..bf8160b 100644 --- a/static/templates/inspect.st +++ b/static/templates/inspect.st @@ -19,10 +19,26 @@ Ursprünglichen Absender beim Weiterleiten verwenden + + + + diff --git a/static/templates/js.st b/static/templates/js.st index a232e55..de98d77 100644 --- a/static/templates/js.st +++ b/static/templates/js.st @@ -27,6 +27,11 @@ function hideList(listEmail){ $.post('/api/list/hide',{list:listEmail},showListResult,'json'); } +function loadArchive(domain,prefix){ + let listEmail = prefix+'@'+domain; + $.get('/api/list/archive?list='+listEmail,showListArchive,'json'); +} + function loadListDetail(listEmail){ $.post('/api/list/detail',{list:listEmail},showListDetail,'json'); } @@ -55,10 +60,26 @@ function showList(listEmail){ $.post('/api/list/show',{list:listEmail},showListResult,'json'); } +function showListArchive(data){ + for (let time in data.archive){ + let post = data.archive[time]; + let row = $(''); + var url = 'post?id='+post.id; + $('').html(''+new Date(post.date)+'').appendTo(row); + $('').html(''+post.from_name+'').appendTo(row); + $('').html(''+post.subject+'').appendTo(row); + row.appendTo($('#archive')); + console.log(post); + } +} + function showListDetail(data){ - console.log(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); } function showListOfEditableLists(data){ @@ -109,7 +130,7 @@ function showListList(data){ for (let i in data.lists){ let list = data.lists[i]; let row = $(''); - $('').text(list.name).appendTo(row); + $('').html(''+list.name+'').appendTo(row); $('',{class:'right'}).text(list.email.prefix).appendTo(row); $('',{class:'right'}).text('@').appendTo(row); $('').text(list.email.domain).appendTo(row); diff --git a/static/templates/post.st b/static/templates/post.st new file mode 100644 index 0000000..7da6a49 --- /dev/null +++ b/static/templates/post.st @@ -0,0 +1,34 @@ + + + + + + + + + + «navigation()» + «userinfo()» + «messages()» +

Widerhall Archive:

+ + + + + + + + + + + + + + + + + +
Date«data.date»
From«data.from_name»
Subject«data.subject»
Content
«data.text»
+ «footer()» + + \ No newline at end of file