diff --git a/pom.xml b/pom.xml index efb6329..a7a89ac 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.1.2 + 0.1.3 diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index aa2cd3a..7cc28ff 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -7,6 +7,8 @@ import de.srsoftware.widerhall.mail.SmtpClient; 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; @@ -15,6 +17,7 @@ import java.io.UnsupportedEncodingException; 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.*; @@ -46,6 +49,7 @@ public class MailingList implements MessageHandler { private static final int VISIBLE = 1; private static final int HIDDEN = 0; private static final int DEFAULT_STATE = STATE_PENDING|STATE_HIDE_RECEIVERS; + private static final String RETAINED_FOLDER = "retained"; private final String name; private final String email; private int state; @@ -136,13 +140,9 @@ public class MailingList implements MessageHandler { 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; } @@ -217,7 +217,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); @@ -312,10 +312,33 @@ public class MailingList implements MessageHandler { } catch (SQLException e){ LOG.error("Was not able to load members of {}; Non-Delivery notification dropped!",this.email(),e); } - } else { - storeMessage(message); - forward(message); + 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; + } } + storeMessage(message); + forward(message); + } + + + + + 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 MailingList open(boolean open) throws SQLException { @@ -346,7 +369,29 @@ public class MailingList implements MessageHandler { 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 @@ -402,7 +447,30 @@ public class MailingList implements MessageHandler { 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); + 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 MailingList setFlag(int flag, boolean on) throws SQLException { @@ -416,6 +484,10 @@ 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); return map; } diff --git a/src/main/java/de/srsoftware/widerhall/mail/ImapClient.java b/src/main/java/de/srsoftware/widerhall/mail/ImapClient.java index 0c0c2a7..e1d58ce 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("Connecting and logging in…"); 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("Connected, opening {}:",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 2c0f875..7f0a043 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java +++ b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java @@ -92,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");