org/sonews/mlgw/Dispatcher.java
author cli
Fri, 21 Aug 2009 17:33:15 +0200
changeset 18 7e527fdf0fa8
parent 16 5a4a41cfc0a3
permissions -rw-r--r--
Fix for #549.
     1 /*
     2  *   SONEWS News Server
     3  *   see AUTHORS for the list of contributors
     4  *
     5  *   This program is free software: you can redistribute it and/or modify
     6  *   it under the terms of the GNU General Public License as published by
     7  *   the Free Software Foundation, either version 3 of the License, or
     8  *   (at your option) any later version.
     9  *
    10  *   This program is distributed in the hope that it will be useful,
    11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  *   GNU General Public License for more details.
    14  *
    15  *   You should have received a copy of the GNU General Public License
    16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  */
    18 
    19 package org.sonews.mlgw;
    20 
    21 import java.io.IOException;
    22 import java.util.ArrayList;
    23 import java.util.List;
    24 import java.util.regex.Matcher;
    25 import java.util.regex.Pattern;
    26 import javax.mail.Address;
    27 import javax.mail.Authenticator;
    28 import javax.mail.Message;
    29 import javax.mail.MessagingException;
    30 import javax.mail.PasswordAuthentication;
    31 import javax.mail.internet.InternetAddress;
    32 import org.sonews.config.Config;
    33 import org.sonews.storage.Article;
    34 import org.sonews.storage.Group;
    35 import org.sonews.storage.Headers;
    36 import org.sonews.storage.StorageBackendException;
    37 import org.sonews.storage.StorageManager;
    38 import org.sonews.util.Log;
    39 import org.sonews.util.Stats;
    40 
    41 /**
    42  * Dispatches messages from mailing list to newsserver or vice versa.
    43  * @author Christian Lins
    44  * @since sonews/0.5.0
    45  */
    46 public class Dispatcher 
    47 {
    48 
    49   static class PasswordAuthenticator extends Authenticator
    50   {
    51     
    52     @Override
    53     public PasswordAuthentication getPasswordAuthentication()
    54     {
    55       final String username = 
    56         Config.inst().get(Config.MLSEND_USER, "user");
    57       final String password = 
    58         Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
    59 
    60       return new PasswordAuthentication(username, password);
    61     }
    62     
    63   }
    64 
    65   /**
    66    * Chunks out the email address of the full List-Post header field.
    67    * @param listPostValue
    68    * @return The matching email address or null
    69    */
    70   private static String chunkListPost(String listPostValue)
    71   {
    72     // listPostValue is of form "<mailto:dev@openoffice.org>"
    73     Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
    74     Matcher mailMatcher = mailPattern.matcher(listPostValue);
    75     if(mailMatcher.find())
    76     {
    77       return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
    78     }
    79     else
    80     {
    81       return null;
    82     }
    83   }
    84 
    85   /**
    86    * This method inspects the header of the given message, trying
    87    * to find the most appropriate recipient.
    88    * @param msg
    89    * @param fallback If this is false only List-Post and X-List-Post headers
    90    *                 are examined.
    91    * @return null or fitting group name for the given message.
    92    */
    93   private static List<String> getGroupFor(final Message msg, final boolean fallback)
    94     throws MessagingException, StorageBackendException
    95   {
    96     List<String> groups = null;
    97 
    98     // Is there a List-Post header?
    99     String[]        listPost = msg.getHeader(Headers.LIST_POST);
   100     InternetAddress listPostAddr;
   101 
   102     if(listPost == null || listPost.length == 0 || "".equals(listPost[0]))
   103     {
   104       // Is there a X-List-Post header?
   105       listPost = msg.getHeader(Headers.X_LIST_POST);
   106     }
   107 
   108     if(listPost != null && listPost.length > 0 
   109       && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null)
   110     {
   111       // listPost[0] is of form "<mailto:dev@openoffice.org>"
   112       listPost[0]  = chunkListPost(listPost[0]);
   113       listPostAddr = new InternetAddress(listPost[0], false);
   114       groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
   115     }
   116     else if(fallback)
   117     {
   118       Log.get().info("Using fallback recipient discovery for: " + msg.getSubject());
   119       groups = new ArrayList<String>();
   120       // Fallback to TO/CC/BCC addresses
   121       Address[] to = msg.getAllRecipients();
   122       for(Address toa : to) // Address can have '<' '>' around
   123       {
   124         if(toa instanceof InternetAddress)
   125         {
   126           List<String> g = StorageManager.current()
   127             .getGroupsForList(((InternetAddress)toa).getAddress());
   128           groups.addAll(g);
   129         }
   130       }
   131     }
   132     
   133     return groups;
   134   }
   135   
   136   /**
   137    * Posts a message that was received from a mailing list to the 
   138    * appropriate newsgroup.
   139    * If the message already exists in the storage, this message checks
   140    * if it must be posted in an additional group. This can happen for
   141    * crosspostings in different mailing lists.
   142    * @param msg
   143    */
   144   public static boolean toGroup(final Message msg)
   145   {
   146     try
   147     {
   148       // Create new Article object
   149       Article article = new Article(msg);
   150       boolean posted  = false;
   151 
   152       // Check if this mail is already existing the storage
   153       boolean updateReq = 
   154         StorageManager.current().isArticleExisting(article.getMessageID());
   155 
   156       List<String> newsgroups = getGroupFor(msg, !updateReq);
   157       List<String> oldgroups  = new ArrayList<String>();
   158       if(updateReq)
   159       {
   160         // Check for duplicate entries of the same group
   161         Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
   162         List<Group> oldGroups = oldArticle.getGroups();
   163         for(Group oldGroup : oldGroups)
   164         {
   165           if(!newsgroups.contains(oldGroup.getName()))
   166           {
   167             oldgroups.add(oldGroup.getName());
   168           }
   169         }
   170       }
   171 
   172       if(newsgroups.size() > 0)
   173       {
   174         newsgroups.addAll(oldgroups);
   175         StringBuilder groups = new StringBuilder();
   176         for(int n = 0; n < newsgroups.size(); n++)
   177         {
   178           groups.append(newsgroups.get(n));
   179           if (n + 1 != newsgroups.size())
   180           {
   181             groups.append(',');
   182           }
   183         }
   184         Log.get().info("Posting to group " + groups.toString());
   185 
   186         article.setGroup(groups.toString());
   187         //article.removeHeader(Headers.REPLY_TO);
   188         //article.removeHeader(Headers.TO);
   189 
   190         // Write article to database
   191         if(updateReq)
   192         {
   193           Log.get().info("Updating " + article.getMessageID()
   194             + " with additional groups");
   195           StorageManager.current().delete(article.getMessageID());
   196           StorageManager.current().addArticle(article);
   197         }
   198         else
   199         {
   200           Log.get().info("Gatewaying " + article.getMessageID() + " to "
   201             + article.getHeader(Headers.NEWSGROUPS)[0]);
   202           StorageManager.current().addArticle(article);
   203           Stats.getInstance().mailGatewayed(
   204             article.getHeader(Headers.NEWSGROUPS)[0]);
   205         }
   206         posted = true;
   207       }
   208       else
   209       {
   210         StringBuilder buf = new StringBuilder();
   211         for (Address toa : msg.getAllRecipients())
   212         {
   213           buf.append(' ');
   214           buf.append(toa.toString());
   215         }
   216         buf.append(" " + article.getHeader(Headers.LIST_POST)[0]);
   217         Log.get().warning("No group for" + buf.toString());
   218       }
   219       return posted;
   220     }
   221     catch(Exception ex)
   222     {
   223       ex.printStackTrace();
   224       return false;
   225     }
   226   }
   227   
   228   /**
   229    * Mails a message received through NNTP to the appropriate mailing list.
   230    * This method MAY be called several times by PostCommand for the same
   231    * article.
   232    */
   233   public static void toList(Article article, String group)
   234     throws IOException, MessagingException, StorageBackendException
   235   {
   236     // Get mailing lists for the group of this article
   237     List<String> rcptAddresses = StorageManager.current().getListsForGroup(group);
   238 
   239     if(rcptAddresses == null || rcptAddresses.size() == 0)
   240     {
   241       Log.get().warning("No ML-address for " + group + " found.");
   242       return;
   243     }
   244 
   245     for(String rcptAddress : rcptAddresses)
   246     {
   247       // Compose message and send it via given SMTP-Host
   248       String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
   249       int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
   250       String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
   251       String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
   252       String smtpFrom = Config.inst().get(
   253         Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
   254 
   255       // TODO: Make Article cloneable()
   256       article.getMessageID(); // Make sure an ID is existing
   257       article.removeHeader(Headers.NEWSGROUPS);
   258       article.removeHeader(Headers.PATH);
   259       article.removeHeader(Headers.LINES);
   260       article.removeHeader(Headers.BYTES);
   261 
   262       article.setHeader("To", rcptAddress);
   263       //article.setHeader("Reply-To", listAddress);
   264 
   265       if (Config.inst().get(Config.MLSEND_RW_SENDER, false))
   266       {
   267         rewriteSenderAddress(article); // Set the SENDER address
   268       }
   269 
   270       SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
   271       smtpTransport.send(article, smtpFrom, rcptAddress);
   272       smtpTransport.close();
   273 
   274       Stats.getInstance().mailGatewayed(group);
   275       Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
   276         + " was delivered to " + rcptAddress + ".");
   277     }
   278   }
   279   
   280   /**
   281    * Sets the SENDER header of the given MimeMessage. This might be necessary
   282    * for moderated groups that does not allow the "normal" FROM sender.
   283    * @param msg
   284    * @throws javax.mail.MessagingException
   285    */
   286   private static void rewriteSenderAddress(Article msg)
   287     throws MessagingException
   288   {
   289     String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
   290 
   291     if(mlAddress != null)
   292     {
   293       msg.setHeader(Headers.SENDER, mlAddress);
   294     }
   295     else
   296     {
   297       throw new MessagingException("Cannot rewrite SENDER header!");
   298     }
   299   }
   300   
   301 }