Merge branch 'main' into lang_de
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 setFlag(int flag, boolean on) throws SQLException {
|
||||
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 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<String,Integer> 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
169
src/main/java/de/srsoftware/widerhall/data/Post.java
Normal file
169
src/main/java/de/srsoftware/widerhall/data/Post.java
Normal file
@@ -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<String, Post> 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<Post> find(MailingList list) throws SQLException {
|
||||
var rs = Database.open().select(TABLE_NAME).where(LIST,list.email()).compile().exec();
|
||||
try {
|
||||
var result = new HashSet<Post>();
|
||||
while (rs.next()) result.add(Post.from(rs));
|
||||
return result;
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static Post from(ResultSet rs) {
|
||||
try {
|
||||
var id = rs.getString(ID);
|
||||
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<String,Object> map() {
|
||||
return Map.of(ID,id,
|
||||
LIST,list.email(),
|
||||
FROM_ADDR,fromAddr,
|
||||
FROM_NAME,fromName,
|
||||
SUBJECT,subject,
|
||||
DATE,timestamp,
|
||||
FILE,filename);
|
||||
}
|
||||
|
||||
public Map<String,Object> 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user