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:
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@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: */
chris@1: public class Dispatcher
chris@1: {
chris@1:
chris@1: static class PasswordAuthenticator extends Authenticator
chris@1: {
chris@1:
chris@1: @Override
chris@1: public PasswordAuthentication getPasswordAuthentication()
chris@1: {
chris@1: final String username =
chris@3: Config.inst().get(Config.MLSEND_USER, "user");
chris@1: final String password =
chris@3: Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
chris@1:
chris@1: return new PasswordAuthentication(username, password);
chris@1: }
chris@1:
chris@1: }
cli@12:
cli@12: /**
cli@12: * Chunks out the email address of the full List-Post header field.
cli@12: * @param listPostValue
cli@12: * @return The matching email address or null
cli@12: */
cli@12: private static String chunkListPost(String listPostValue)
cli@12: {
cli@12: // listPostValue is of form ""
cli@12: Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
cli@12: Matcher mailMatcher = mailPattern.matcher(listPostValue);
cli@12: if(mailMatcher.find())
cli@12: {
cli@12: return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
cli@12: }
cli@12: else
cli@12: {
cli@12: return null;
cli@12: }
cli@12: }
cli@12:
cli@12: /**
cli@12: * This method inspects the header of the given message, trying
cli@12: * to find the most appropriate recipient.
cli@12: * @param msg
cli@12: * @param fallback If this is false only List-Post and X-List-Post headers
cli@12: * are examined.
cli@12: * @return null or fitting group name for the given message.
cli@12: */
cli@12: private static List getGroupFor(final Message msg, final boolean fallback)
cli@12: throws MessagingException, StorageBackendException
cli@12: {
cli@12: List groups = null;
cli@12:
cli@12: // Is there a List-Post header?
cli@12: String[] listPost = msg.getHeader(Headers.LIST_POST);
cli@12: InternetAddress listPostAddr;
cli@12:
cli@12: if(listPost == null || listPost.length == 0 || "".equals(listPost[0]))
cli@12: {
cli@12: // Is there a X-List-Post header?
cli@12: listPost = msg.getHeader(Headers.X_LIST_POST);
cli@12: }
cli@12:
cli@12: if(listPost != null && listPost.length > 0
cli@12: && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null)
cli@12: {
cli@12: // listPost[0] is of form ""
cli@12: listPost[0] = chunkListPost(listPost[0]);
cli@12: listPostAddr = new InternetAddress(listPost[0], false);
cli@14: groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
cli@12: }
cli@12: else if(fallback)
cli@12: {
cli@16: Log.get().info("Using fallback recipient discovery for: " + msg.getSubject());
cli@12: groups = new ArrayList();
cli@12: // Fallback to TO/CC/BCC addresses
cli@12: Address[] to = msg.getAllRecipients();
cli@12: for(Address toa : to) // Address can have '<' '>' around
cli@12: {
cli@12: if(toa instanceof InternetAddress)
cli@12: {
cli@14: List g = StorageManager.current()
cli@14: .getGroupsForList(((InternetAddress)toa).getAddress());
cli@12: groups.addAll(g);
cli@12: }
cli@12: }
cli@12: }
cli@12:
cli@12: return groups;
cli@12: }
chris@1:
chris@1: /**
chris@1: * Posts a message that was received from a mailing list to the
chris@1: * appropriate newsgroup.
cli@12: * If the message already exists in the storage, this message checks
cli@12: * if it must be posted in an additional group. This can happen for
cli@12: * crosspostings in different mailing lists.
chris@1: * @param msg
chris@1: */
chris@1: public static boolean toGroup(final Message msg)
chris@1: {
cli@36: if(msg == null)
cli@36: {
cli@36: throw new IllegalArgumentException("Argument 'msg' must not be null!");
cli@36: }
cli@36:
chris@1: try
chris@1: {
cli@12: // Create new Article object
cli@12: Article article = new Article(msg);
cli@12: boolean posted = false;
cli@12:
cli@12: // Check if this mail is already existing the storage
cli@12: boolean updateReq =
cli@12: StorageManager.current().isArticleExisting(article.getMessageID());
cli@12:
cli@12: List newsgroups = getGroupFor(msg, !updateReq);
cli@12: List oldgroups = new ArrayList();
cli@12: if(updateReq)
chris@1: {
cli@12: // Check for duplicate entries of the same group
cli@12: Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
cli@36: if(oldArticle != null)
cli@36: {
cli@36: List oldGroups = oldArticle.getGroups();
cli@36: for(Group oldGroup : oldGroups)
cli@12: {
cli@36: if(!newsgroups.contains(oldGroup.getName()))
cli@36: {
cli@36: oldgroups.add(oldGroup.getName());
cli@36: }
cli@12: }
cli@36: }
chris@3: }
chris@3:
cli@12: if(newsgroups.size() > 0)
chris@3: {
cli@12: newsgroups.addAll(oldgroups);
cli@12: StringBuilder groups = new StringBuilder();
cli@12: for(int n = 0; n < newsgroups.size(); n++)
cli@12: {
cli@12: groups.append(newsgroups.get(n));
cli@12: if (n + 1 != newsgroups.size())
cli@12: {
cli@12: groups.append(',');
cli@12: }
cli@12: }
cli@16: Log.get().info("Posting to group " + groups.toString());
cli@12:
cli@12: article.setGroup(groups.toString());
cli@12: //article.removeHeader(Headers.REPLY_TO);
cli@12: //article.removeHeader(Headers.TO);
cli@12:
cli@12: // Write article to database
cli@12: if(updateReq)
cli@12: {
cli@16: Log.get().info("Updating " + article.getMessageID()
cli@16: + " with additional groups");
cli@12: StorageManager.current().delete(article.getMessageID());
cli@12: StorageManager.current().addArticle(article);
cli@12: }
cli@12: else
cli@12: {
cli@16: Log.get().info("Gatewaying " + article.getMessageID() + " to "
cli@16: + article.getHeader(Headers.NEWSGROUPS)[0]);
cli@12: StorageManager.current().addArticle(article);
cli@12: Stats.getInstance().mailGatewayed(
cli@12: article.getHeader(Headers.NEWSGROUPS)[0]);
cli@12: }
cli@12: posted = true;
chris@1: }
chris@1: else
chris@1: {
cli@12: StringBuilder buf = new StringBuilder();
cli@12: for (Address toa : msg.getAllRecipients())
chris@1: {
cli@12: buf.append(' ');
cli@12: buf.append(toa.toString());
chris@3: }
cli@12: buf.append(" " + article.getHeader(Headers.LIST_POST)[0]);
cli@16: Log.get().warning("No group for" + buf.toString());
chris@1: }
cli@12: return posted;
chris@1: }
chris@1: catch(Exception ex)
chris@1: {
chris@1: ex.printStackTrace();
chris@1: return false;
chris@1: }
chris@1: }
chris@1:
chris@1: /**
chris@1: * Mails a message received through NNTP to the appropriate mailing list.
cli@12: * This method MAY be called several times by PostCommand for the same
cli@12: * article.
chris@1: */
cli@12: public static void toList(Article article, String group)
chris@3: throws IOException, MessagingException, StorageBackendException
chris@1: {
chris@1: // Get mailing lists for the group of this article
cli@12: List rcptAddresses = StorageManager.current().getListsForGroup(group);
cli@12:
cli@12: if(rcptAddresses == null || rcptAddresses.size() == 0)
chris@1: {
cli@16: Log.get().warning("No ML-address for " + group + " found.");
cli@12: return;
chris@1: }
chris@1:
cli@12: for(String rcptAddress : rcptAddresses)
chris@1: {
chris@1: // Compose message and send it via given SMTP-Host
chris@3: String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
cli@12: int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
chris@3: String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
cli@12: String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
chris@3: String smtpFrom = Config.inst().get(
cli@12: Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
chris@1:
chris@3: // TODO: Make Article cloneable()
chris@3: article.getMessageID(); // Make sure an ID is existing
chris@3: article.removeHeader(Headers.NEWSGROUPS);
chris@3: article.removeHeader(Headers.PATH);
chris@3: article.removeHeader(Headers.LINES);
chris@3: article.removeHeader(Headers.BYTES);
chris@1:
cli@12: article.setHeader("To", rcptAddress);
cli@12: //article.setHeader("Reply-To", listAddress);
chris@1:
cli@12: if (Config.inst().get(Config.MLSEND_RW_SENDER, false))
chris@1: {
chris@3: rewriteSenderAddress(article); // Set the SENDER address
chris@1: }
chris@1:
chris@3: SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
cli@12: smtpTransport.send(article, smtpFrom, rcptAddress);
chris@3: smtpTransport.close();
chris@1:
chris@3: Stats.getInstance().mailGatewayed(group);
cli@16: Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
cli@16: + " was delivered to " + rcptAddress + ".");
chris@1: }
chris@1: }
chris@1:
chris@1: /**
chris@1: * Sets the SENDER header of the given MimeMessage. This might be necessary
chris@1: * for moderated groups that does not allow the "normal" FROM sender.
chris@1: * @param msg
chris@1: * @throws javax.mail.MessagingException
chris@1: */
chris@3: private static void rewriteSenderAddress(Article msg)
chris@1: throws MessagingException
chris@1: {
chris@3: String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
chris@1:
chris@1: if(mlAddress != null)
chris@1: {
chris@3: msg.setHeader(Headers.SENDER, mlAddress);
chris@1: }
chris@1: else
chris@1: {
chris@1: throw new MessagingException("Cannot rewrite SENDER header!");
chris@1: }
chris@1: }
chris@1:
chris@1: }