chris@1: /* chris@1: * SONEWS News Server chris@1: * see AUTHORS for the list of contributors chris@1: * chris@1: * This program is free software: you can redistribute it and/or modify chris@1: * it under the terms of the GNU General Public License as published by chris@1: * the Free Software Foundation, either version 3 of the License, or chris@1: * (at your option) any later version. chris@1: * chris@1: * This program is distributed in the hope that it will be useful, chris@1: * but WITHOUT ANY WARRANTY; without even the implied warranty of chris@1: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the chris@1: * GNU General Public License for more details. chris@1: * chris@1: * You should have received a copy of the GNU General Public License chris@1: * along with this program. If not, see . chris@1: */ chris@1: package org.sonews.mlgw; chris@1: chris@1: import java.io.IOException; chris@1: import java.util.ArrayList; chris@1: import java.util.List; cli@50: import java.util.logging.Level; cli@12: import java.util.regex.Matcher; cli@12: import java.util.regex.Pattern; chris@1: import javax.mail.Address; chris@1: import javax.mail.Authenticator; chris@1: import javax.mail.Message; chris@1: import javax.mail.MessagingException; chris@1: import javax.mail.PasswordAuthentication; chris@1: import javax.mail.internet.InternetAddress; chris@3: import org.sonews.config.Config; chris@3: import org.sonews.storage.Article; cli@12: import org.sonews.storage.Group; chris@3: import org.sonews.storage.Headers; chris@3: import org.sonews.storage.StorageBackendException; chris@3: import org.sonews.storage.StorageManager; chris@1: import org.sonews.util.Log; chris@1: import org.sonews.util.Stats; chris@1: chris@1: /** cli@12: * Dispatches messages from mailing list to newsserver or vice versa. chris@1: * @author Christian Lins chris@1: * @since sonews/0.5.0 chris@1: */ cli@50: public class Dispatcher { chris@1: cli@50: static class PasswordAuthenticator extends Authenticator { chris@1: cli@37: @Override cli@50: public PasswordAuthentication getPasswordAuthentication() { cli@37: final String username = cli@50: Config.inst().get(Config.MLSEND_USER, "user"); cli@37: final String password = cli@50: Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); cli@12: cli@37: return new PasswordAuthentication(username, password); cli@37: } cli@37: } cli@12: cli@37: /** cli@37: * Chunks out the email address of the full List-Post header field. cli@37: * @param listPostValue cli@37: * @return The matching email address or null cli@37: */ cli@50: private static String chunkListPost(String listPostValue) { cli@37: // listPostValue is of form "" cli@37: Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+"); cli@37: Matcher mailMatcher = mailPattern.matcher(listPostValue); cli@37: if (mailMatcher.find()) { cli@37: return listPostValue.substring(mailMatcher.start(), mailMatcher.end()); cli@37: } else { cli@37: return null; cli@37: } cli@37: } cli@12: cli@37: /** cli@37: * This method inspects the header of the given message, trying cli@37: * to find the most appropriate recipient. cli@37: * @param msg cli@37: * @param fallback If this is false only List-Post and X-List-Post headers cli@37: * are examined. cli@37: * @return null or fitting group name for the given message. cli@37: */ cli@37: private static List getGroupFor(final Message msg, final boolean fallback) cli@50: throws MessagingException, StorageBackendException { cli@37: List groups = null; cli@12: cli@37: // Is there a List-Post header? cli@37: String[] listPost = msg.getHeader(Headers.LIST_POST); cli@37: InternetAddress listPostAddr; cli@12: cli@37: if (listPost == null || listPost.length == 0 || "".equals(listPost[0])) { cli@37: // Is there a X-List-Post header? cli@37: listPost = msg.getHeader(Headers.X_LIST_POST); cli@37: } cli@37: cli@37: if (listPost != null && listPost.length > 0 cli@50: && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null) { cli@37: // listPost[0] is of form "" cli@37: listPost[0] = chunkListPost(listPost[0]); cli@37: listPostAddr = new InternetAddress(listPost[0], false); cli@37: groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress()); cli@37: } else if (fallback) { cli@50: StringBuilder strBuf = new StringBuilder(); cli@50: strBuf.append("Using fallback recipient discovery for: "); cli@50: strBuf.append(msg.getSubject()); cli@50: Log.get().info(strBuf.toString()); cli@37: groups = new ArrayList(); cli@37: // Fallback to TO/CC/BCC addresses cli@37: Address[] to = msg.getAllRecipients(); cli@37: for (Address toa : to) // Address can have '<' '>' around cli@37: { cli@37: if (toa instanceof InternetAddress) { cli@37: List g = StorageManager.current().getGroupsForList(((InternetAddress) toa).getAddress()); cli@37: groups.addAll(g); cli@37: } cli@37: } cli@37: } cli@37: cli@37: return groups; cli@37: } cli@37: cli@37: /** cli@37: * Posts a message that was received from a mailing list to the cli@37: * appropriate newsgroup. cli@37: * If the message already exists in the storage, this message checks cli@37: * if it must be posted in an additional group. This can happen for cli@37: * crosspostings in different mailing lists. cli@37: * @param msg cli@37: */ cli@50: public static boolean toGroup(final Message msg) { cli@37: if (msg == null) { cli@37: throw new IllegalArgumentException("Argument 'msg' must not be null!"); cli@37: } cli@36: cli@37: try { cli@37: // Create new Article object cli@37: Article article = new Article(msg); cli@37: boolean posted = false; cli@12: cli@37: // Check if this mail is already existing the storage cli@37: boolean updateReq = cli@50: StorageManager.current().isArticleExisting(article.getMessageID()); cli@12: cli@37: List newsgroups = getGroupFor(msg, !updateReq); cli@37: List oldgroups = new ArrayList(); cli@37: if (updateReq) { cli@37: // Check for duplicate entries of the same group cli@37: Article oldArticle = StorageManager.current().getArticle(article.getMessageID()); cli@37: if (oldArticle != null) { cli@37: List oldGroups = oldArticle.getGroups(); cli@37: for (Group oldGroup : oldGroups) { cli@37: if (!newsgroups.contains(oldGroup.getName())) { cli@37: oldgroups.add(oldGroup.getName()); cli@37: } cli@37: } cli@37: } cli@37: } cli@37: cli@37: if (newsgroups.size() > 0) { cli@37: newsgroups.addAll(oldgroups); cli@37: StringBuilder groups = new StringBuilder(); cli@37: for (int n = 0; n < newsgroups.size(); n++) { cli@37: groups.append(newsgroups.get(n)); cli@37: if (n + 1 != newsgroups.size()) { cli@37: groups.append(','); cli@37: } cli@37: } cli@50: cli@50: StringBuilder strBuf = new StringBuilder(); cli@50: strBuf.append("Posting to group "); cli@50: strBuf.append(groups.toString()); cli@50: Log.get().info(strBuf.toString()); cli@37: cli@37: article.setGroup(groups.toString()); cli@37: //article.removeHeader(Headers.REPLY_TO); cli@37: //article.removeHeader(Headers.TO); cli@37: cli@37: // Write article to database cli@37: if (updateReq) { cli@37: Log.get().info("Updating " + article.getMessageID() cli@50: + " with additional groups"); cli@37: StorageManager.current().delete(article.getMessageID()); cli@37: StorageManager.current().addArticle(article); cli@37: } else { cli@37: Log.get().info("Gatewaying " + article.getMessageID() + " to " cli@50: + article.getHeader(Headers.NEWSGROUPS)[0]); cli@37: StorageManager.current().addArticle(article); cli@37: Stats.getInstance().mailGatewayed( cli@50: article.getHeader(Headers.NEWSGROUPS)[0]); cli@37: } cli@37: posted = true; cli@37: } else { cli@37: StringBuilder buf = new StringBuilder(); cli@37: for (Address toa : msg.getAllRecipients()) { cli@37: buf.append(' '); cli@37: buf.append(toa.toString()); cli@37: } cli@50: buf.append(" "); cli@50: buf.append(article.getHeader(Headers.LIST_POST)[0]); cli@37: Log.get().warning("No group for" + buf.toString()); cli@37: } cli@37: return posted; cli@37: } catch (Exception ex) { cli@50: Log.get().log(Level.WARNING, ex.getLocalizedMessage(), ex); cli@37: return false; cli@36: } cli@37: } chris@3: cli@37: /** cli@37: * Mails a message received through NNTP to the appropriate mailing list. cli@37: * This method MAY be called several times by PostCommand for the same cli@37: * article. cli@37: */ cli@50: public static boolean toList(Article article, String group) cli@50: throws IOException, MessagingException, StorageBackendException { cli@37: // Get mailing lists for the group of this article cli@37: List rcptAddresses = StorageManager.current().getListsForGroup(group); cli@12: cli@50: if (rcptAddresses == null || rcptAddresses.isEmpty()) { cli@50: StringBuilder strBuf = new StringBuilder(); cli@50: strBuf.append("No ML address found for group "); cli@50: strBuf.append(group); cli@50: Log.get().warning(strBuf.toString()); cli@50: return false; cli@37: } cli@12: cli@37: for (String rcptAddress : rcptAddresses) { cli@37: // Compose message and send it via given SMTP-Host cli@37: String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost"); cli@37: int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25); cli@37: String smtpUser = Config.inst().get(Config.MLSEND_USER, "user"); cli@37: String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); cli@37: String smtpFrom = Config.inst().get( cli@50: Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]); cli@12: cli@37: // TODO: Make Article cloneable() cli@37: article.getMessageID(); // Make sure an ID is existing cli@37: article.removeHeader(Headers.NEWSGROUPS); cli@37: article.removeHeader(Headers.PATH); cli@37: article.removeHeader(Headers.LINES); cli@37: article.removeHeader(Headers.BYTES); chris@1: cli@37: article.setHeader("To", rcptAddress); cli@37: //article.setHeader("Reply-To", listAddress); chris@1: cli@37: if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) { cli@37: rewriteSenderAddress(article); // Set the SENDER address cli@37: } chris@1: cli@37: SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort); cli@37: smtpTransport.send(article, smtpFrom, rcptAddress); cli@37: smtpTransport.close(); chris@1: cli@37: Stats.getInstance().mailGatewayed(group); cli@37: Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0] cli@50: + " was delivered to " + rcptAddress + "."); cli@37: } cli@50: return true; cli@37: } chris@1: cli@37: /** cli@37: * Sets the SENDER header of the given MimeMessage. This might be necessary cli@37: * for moderated groups that does not allow the "normal" FROM sender. cli@37: * @param msg cli@37: * @throws javax.mail.MessagingException cli@37: */ cli@37: private static void rewriteSenderAddress(Article msg) cli@50: throws MessagingException { cli@37: String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null); chris@1: cli@37: if (mlAddress != null) { cli@37: msg.setHeader(Headers.SENDER, mlAddress); cli@37: } else { cli@37: throw new MessagingException("Cannot rewrite SENDER header!"); cli@37: } cli@37: } chris@1: }