src/org/sonews/mlgw/Dispatcher.java
changeset 35 ed84c8bdd87b
parent 16 5a4a41cfc0a3
child 36 c404a87db5b7
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/org/sonews/mlgw/Dispatcher.java	Sun Aug 29 17:28:58 2010 +0200
     1.3 @@ -0,0 +1,301 @@
     1.4 +/*
     1.5 + *   SONEWS News Server
     1.6 + *   see AUTHORS for the list of contributors
     1.7 + *
     1.8 + *   This program is free software: you can redistribute it and/or modify
     1.9 + *   it under the terms of the GNU General Public License as published by
    1.10 + *   the Free Software Foundation, either version 3 of the License, or
    1.11 + *   (at your option) any later version.
    1.12 + *
    1.13 + *   This program is distributed in the hope that it will be useful,
    1.14 + *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    1.15 + *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    1.16 + *   GNU General Public License for more details.
    1.17 + *
    1.18 + *   You should have received a copy of the GNU General Public License
    1.19 + *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    1.20 + */
    1.21 +
    1.22 +package org.sonews.mlgw;
    1.23 +
    1.24 +import java.io.IOException;
    1.25 +import java.util.ArrayList;
    1.26 +import java.util.List;
    1.27 +import java.util.regex.Matcher;
    1.28 +import java.util.regex.Pattern;
    1.29 +import javax.mail.Address;
    1.30 +import javax.mail.Authenticator;
    1.31 +import javax.mail.Message;
    1.32 +import javax.mail.MessagingException;
    1.33 +import javax.mail.PasswordAuthentication;
    1.34 +import javax.mail.internet.InternetAddress;
    1.35 +import org.sonews.config.Config;
    1.36 +import org.sonews.storage.Article;
    1.37 +import org.sonews.storage.Group;
    1.38 +import org.sonews.storage.Headers;
    1.39 +import org.sonews.storage.StorageBackendException;
    1.40 +import org.sonews.storage.StorageManager;
    1.41 +import org.sonews.util.Log;
    1.42 +import org.sonews.util.Stats;
    1.43 +
    1.44 +/**
    1.45 + * Dispatches messages from mailing list to newsserver or vice versa.
    1.46 + * @author Christian Lins
    1.47 + * @since sonews/0.5.0
    1.48 + */
    1.49 +public class Dispatcher 
    1.50 +{
    1.51 +
    1.52 +  static class PasswordAuthenticator extends Authenticator
    1.53 +  {
    1.54 +    
    1.55 +    @Override
    1.56 +    public PasswordAuthentication getPasswordAuthentication()
    1.57 +    {
    1.58 +      final String username = 
    1.59 +        Config.inst().get(Config.MLSEND_USER, "user");
    1.60 +      final String password = 
    1.61 +        Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
    1.62 +
    1.63 +      return new PasswordAuthentication(username, password);
    1.64 +    }
    1.65 +    
    1.66 +  }
    1.67 +
    1.68 +  /**
    1.69 +   * Chunks out the email address of the full List-Post header field.
    1.70 +   * @param listPostValue
    1.71 +   * @return The matching email address or null
    1.72 +   */
    1.73 +  private static String chunkListPost(String listPostValue)
    1.74 +  {
    1.75 +    // listPostValue is of form "<mailto:dev@openoffice.org>"
    1.76 +    Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
    1.77 +    Matcher mailMatcher = mailPattern.matcher(listPostValue);
    1.78 +    if(mailMatcher.find())
    1.79 +    {
    1.80 +      return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
    1.81 +    }
    1.82 +    else
    1.83 +    {
    1.84 +      return null;
    1.85 +    }
    1.86 +  }
    1.87 +
    1.88 +  /**
    1.89 +   * This method inspects the header of the given message, trying
    1.90 +   * to find the most appropriate recipient.
    1.91 +   * @param msg
    1.92 +   * @param fallback If this is false only List-Post and X-List-Post headers
    1.93 +   *                 are examined.
    1.94 +   * @return null or fitting group name for the given message.
    1.95 +   */
    1.96 +  private static List<String> getGroupFor(final Message msg, final boolean fallback)
    1.97 +    throws MessagingException, StorageBackendException
    1.98 +  {
    1.99 +    List<String> groups = null;
   1.100 +
   1.101 +    // Is there a List-Post header?
   1.102 +    String[]        listPost = msg.getHeader(Headers.LIST_POST);
   1.103 +    InternetAddress listPostAddr;
   1.104 +
   1.105 +    if(listPost == null || listPost.length == 0 || "".equals(listPost[0]))
   1.106 +    {
   1.107 +      // Is there a X-List-Post header?
   1.108 +      listPost = msg.getHeader(Headers.X_LIST_POST);
   1.109 +    }
   1.110 +
   1.111 +    if(listPost != null && listPost.length > 0 
   1.112 +      && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null)
   1.113 +    {
   1.114 +      // listPost[0] is of form "<mailto:dev@openoffice.org>"
   1.115 +      listPost[0]  = chunkListPost(listPost[0]);
   1.116 +      listPostAddr = new InternetAddress(listPost[0], false);
   1.117 +      groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
   1.118 +    }
   1.119 +    else if(fallback)
   1.120 +    {
   1.121 +      Log.get().info("Using fallback recipient discovery for: " + msg.getSubject());
   1.122 +      groups = new ArrayList<String>();
   1.123 +      // Fallback to TO/CC/BCC addresses
   1.124 +      Address[] to = msg.getAllRecipients();
   1.125 +      for(Address toa : to) // Address can have '<' '>' around
   1.126 +      {
   1.127 +        if(toa instanceof InternetAddress)
   1.128 +        {
   1.129 +          List<String> g = StorageManager.current()
   1.130 +            .getGroupsForList(((InternetAddress)toa).getAddress());
   1.131 +          groups.addAll(g);
   1.132 +        }
   1.133 +      }
   1.134 +    }
   1.135 +    
   1.136 +    return groups;
   1.137 +  }
   1.138 +  
   1.139 +  /**
   1.140 +   * Posts a message that was received from a mailing list to the 
   1.141 +   * appropriate newsgroup.
   1.142 +   * If the message already exists in the storage, this message checks
   1.143 +   * if it must be posted in an additional group. This can happen for
   1.144 +   * crosspostings in different mailing lists.
   1.145 +   * @param msg
   1.146 +   */
   1.147 +  public static boolean toGroup(final Message msg)
   1.148 +  {
   1.149 +    try
   1.150 +    {
   1.151 +      // Create new Article object
   1.152 +      Article article = new Article(msg);
   1.153 +      boolean posted  = false;
   1.154 +
   1.155 +      // Check if this mail is already existing the storage
   1.156 +      boolean updateReq = 
   1.157 +        StorageManager.current().isArticleExisting(article.getMessageID());
   1.158 +
   1.159 +      List<String> newsgroups = getGroupFor(msg, !updateReq);
   1.160 +      List<String> oldgroups  = new ArrayList<String>();
   1.161 +      if(updateReq)
   1.162 +      {
   1.163 +        // Check for duplicate entries of the same group
   1.164 +        Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
   1.165 +        List<Group> oldGroups = oldArticle.getGroups();
   1.166 +        for(Group oldGroup : oldGroups)
   1.167 +        {
   1.168 +          if(!newsgroups.contains(oldGroup.getName()))
   1.169 +          {
   1.170 +            oldgroups.add(oldGroup.getName());
   1.171 +          }
   1.172 +        }
   1.173 +      }
   1.174 +
   1.175 +      if(newsgroups.size() > 0)
   1.176 +      {
   1.177 +        newsgroups.addAll(oldgroups);
   1.178 +        StringBuilder groups = new StringBuilder();
   1.179 +        for(int n = 0; n < newsgroups.size(); n++)
   1.180 +        {
   1.181 +          groups.append(newsgroups.get(n));
   1.182 +          if (n + 1 != newsgroups.size())
   1.183 +          {
   1.184 +            groups.append(',');
   1.185 +          }
   1.186 +        }
   1.187 +        Log.get().info("Posting to group " + groups.toString());
   1.188 +
   1.189 +        article.setGroup(groups.toString());
   1.190 +        //article.removeHeader(Headers.REPLY_TO);
   1.191 +        //article.removeHeader(Headers.TO);
   1.192 +
   1.193 +        // Write article to database
   1.194 +        if(updateReq)
   1.195 +        {
   1.196 +          Log.get().info("Updating " + article.getMessageID()
   1.197 +            + " with additional groups");
   1.198 +          StorageManager.current().delete(article.getMessageID());
   1.199 +          StorageManager.current().addArticle(article);
   1.200 +        }
   1.201 +        else
   1.202 +        {
   1.203 +          Log.get().info("Gatewaying " + article.getMessageID() + " to "
   1.204 +            + article.getHeader(Headers.NEWSGROUPS)[0]);
   1.205 +          StorageManager.current().addArticle(article);
   1.206 +          Stats.getInstance().mailGatewayed(
   1.207 +            article.getHeader(Headers.NEWSGROUPS)[0]);
   1.208 +        }
   1.209 +        posted = true;
   1.210 +      }
   1.211 +      else
   1.212 +      {
   1.213 +        StringBuilder buf = new StringBuilder();
   1.214 +        for (Address toa : msg.getAllRecipients())
   1.215 +        {
   1.216 +          buf.append(' ');
   1.217 +          buf.append(toa.toString());
   1.218 +        }
   1.219 +        buf.append(" " + article.getHeader(Headers.LIST_POST)[0]);
   1.220 +        Log.get().warning("No group for" + buf.toString());
   1.221 +      }
   1.222 +      return posted;
   1.223 +    }
   1.224 +    catch(Exception ex)
   1.225 +    {
   1.226 +      ex.printStackTrace();
   1.227 +      return false;
   1.228 +    }
   1.229 +  }
   1.230 +  
   1.231 +  /**
   1.232 +   * Mails a message received through NNTP to the appropriate mailing list.
   1.233 +   * This method MAY be called several times by PostCommand for the same
   1.234 +   * article.
   1.235 +   */
   1.236 +  public static void toList(Article article, String group)
   1.237 +    throws IOException, MessagingException, StorageBackendException
   1.238 +  {
   1.239 +    // Get mailing lists for the group of this article
   1.240 +    List<String> rcptAddresses = StorageManager.current().getListsForGroup(group);
   1.241 +
   1.242 +    if(rcptAddresses == null || rcptAddresses.size() == 0)
   1.243 +    {
   1.244 +      Log.get().warning("No ML-address for " + group + " found.");
   1.245 +      return;
   1.246 +    }
   1.247 +
   1.248 +    for(String rcptAddress : rcptAddresses)
   1.249 +    {
   1.250 +      // Compose message and send it via given SMTP-Host
   1.251 +      String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
   1.252 +      int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
   1.253 +      String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
   1.254 +      String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
   1.255 +      String smtpFrom = Config.inst().get(
   1.256 +        Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
   1.257 +
   1.258 +      // TODO: Make Article cloneable()
   1.259 +      article.getMessageID(); // Make sure an ID is existing
   1.260 +      article.removeHeader(Headers.NEWSGROUPS);
   1.261 +      article.removeHeader(Headers.PATH);
   1.262 +      article.removeHeader(Headers.LINES);
   1.263 +      article.removeHeader(Headers.BYTES);
   1.264 +
   1.265 +      article.setHeader("To", rcptAddress);
   1.266 +      //article.setHeader("Reply-To", listAddress);
   1.267 +
   1.268 +      if (Config.inst().get(Config.MLSEND_RW_SENDER, false))
   1.269 +      {
   1.270 +        rewriteSenderAddress(article); // Set the SENDER address
   1.271 +      }
   1.272 +
   1.273 +      SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
   1.274 +      smtpTransport.send(article, smtpFrom, rcptAddress);
   1.275 +      smtpTransport.close();
   1.276 +
   1.277 +      Stats.getInstance().mailGatewayed(group);
   1.278 +      Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
   1.279 +        + " was delivered to " + rcptAddress + ".");
   1.280 +    }
   1.281 +  }
   1.282 +  
   1.283 +  /**
   1.284 +   * Sets the SENDER header of the given MimeMessage. This might be necessary
   1.285 +   * for moderated groups that does not allow the "normal" FROM sender.
   1.286 +   * @param msg
   1.287 +   * @throws javax.mail.MessagingException
   1.288 +   */
   1.289 +  private static void rewriteSenderAddress(Article msg)
   1.290 +    throws MessagingException
   1.291 +  {
   1.292 +    String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
   1.293 +
   1.294 +    if(mlAddress != null)
   1.295 +    {
   1.296 +      msg.setHeader(Headers.SENDER, mlAddress);
   1.297 +    }
   1.298 +    else
   1.299 +    {
   1.300 +      throw new MessagingException("Cannot rewrite SENDER header!");
   1.301 +    }
   1.302 +  }
   1.303 +  
   1.304 +}