src/org/sonews/mlgw/Dispatcher.java
author cli
Sun Sep 11 17:36:47 2011 +0200 (2011-09-11)
changeset 50 0bf10add82d9
parent 37 74139325d305
child 58 b2df305a13ce
permissions -rwxr-xr-x
Fix for issue #17. Error when posting to mailinglist is now reported back to user as NNTP error.
     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 package org.sonews.mlgw;
    19 
    20 import java.io.IOException;
    21 import java.util.ArrayList;
    22 import java.util.List;
    23 import java.util.logging.Level;
    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 	static class PasswordAuthenticator extends Authenticator {
    49 
    50 		@Override
    51 		public PasswordAuthentication getPasswordAuthentication() {
    52 			final String username =
    53 					Config.inst().get(Config.MLSEND_USER, "user");
    54 			final String password =
    55 					Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
    56 
    57 			return new PasswordAuthentication(username, password);
    58 		}
    59 	}
    60 
    61 	/**
    62 	 * Chunks out the email address of the full List-Post header field.
    63 	 * @param listPostValue
    64 	 * @return The matching email address or null
    65 	 */
    66 	private static String chunkListPost(String listPostValue) {
    67 		// listPostValue is of form "<mailto:dev@openoffice.org>"
    68 		Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
    69 		Matcher mailMatcher = mailPattern.matcher(listPostValue);
    70 		if (mailMatcher.find()) {
    71 			return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
    72 		} else {
    73 			return null;
    74 		}
    75 	}
    76 
    77 	/**
    78 	 * This method inspects the header of the given message, trying
    79 	 * to find the most appropriate recipient.
    80 	 * @param msg
    81 	 * @param fallback If this is false only List-Post and X-List-Post headers
    82 	 *                 are examined.
    83 	 * @return null or fitting group name for the given message.
    84 	 */
    85 	private static List<String> getGroupFor(final Message msg, final boolean fallback)
    86 			throws MessagingException, StorageBackendException {
    87 		List<String> groups = null;
    88 
    89 		// Is there a List-Post header?
    90 		String[] listPost = msg.getHeader(Headers.LIST_POST);
    91 		InternetAddress listPostAddr;
    92 
    93 		if (listPost == null || listPost.length == 0 || "".equals(listPost[0])) {
    94 			// Is there a X-List-Post header?
    95 			listPost = msg.getHeader(Headers.X_LIST_POST);
    96 		}
    97 
    98 		if (listPost != null && listPost.length > 0
    99 				&& !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null) {
   100 			// listPost[0] is of form "<mailto:dev@openoffice.org>"
   101 			listPost[0] = chunkListPost(listPost[0]);
   102 			listPostAddr = new InternetAddress(listPost[0], false);
   103 			groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
   104 		} else if (fallback) {
   105 			StringBuilder strBuf = new StringBuilder();
   106 			strBuf.append("Using fallback recipient discovery for: ");
   107 			strBuf.append(msg.getSubject());
   108 			Log.get().info(strBuf.toString());
   109 			groups = new ArrayList<String>();
   110 			// Fallback to TO/CC/BCC addresses
   111 			Address[] to = msg.getAllRecipients();
   112 			for (Address toa : to) // Address can have '<' '>' around
   113 			{
   114 				if (toa instanceof InternetAddress) {
   115 					List<String> g = StorageManager.current().getGroupsForList(((InternetAddress) toa).getAddress());
   116 					groups.addAll(g);
   117 				}
   118 			}
   119 		}
   120 
   121 		return groups;
   122 	}
   123 
   124 	/**
   125 	 * Posts a message that was received from a mailing list to the
   126 	 * appropriate newsgroup.
   127 	 * If the message already exists in the storage, this message checks
   128 	 * if it must be posted in an additional group. This can happen for
   129 	 * crosspostings in different mailing lists.
   130 	 * @param msg
   131 	 */
   132 	public static boolean toGroup(final Message msg) {
   133 		if (msg == null) {
   134 			throw new IllegalArgumentException("Argument 'msg' must not be null!");
   135 		}
   136 
   137 		try {
   138 			// Create new Article object
   139 			Article article = new Article(msg);
   140 			boolean posted = false;
   141 
   142 			// Check if this mail is already existing the storage
   143 			boolean updateReq =
   144 					StorageManager.current().isArticleExisting(article.getMessageID());
   145 
   146 			List<String> newsgroups = getGroupFor(msg, !updateReq);
   147 			List<String> oldgroups = new ArrayList<String>();
   148 			if (updateReq) {
   149 				// Check for duplicate entries of the same group
   150 				Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
   151 				if (oldArticle != null) {
   152 					List<Group> oldGroups = oldArticle.getGroups();
   153 					for (Group oldGroup : oldGroups) {
   154 						if (!newsgroups.contains(oldGroup.getName())) {
   155 							oldgroups.add(oldGroup.getName());
   156 						}
   157 					}
   158 				}
   159 			}
   160 
   161 			if (newsgroups.size() > 0) {
   162 				newsgroups.addAll(oldgroups);
   163 				StringBuilder groups = new StringBuilder();
   164 				for (int n = 0; n < newsgroups.size(); n++) {
   165 					groups.append(newsgroups.get(n));
   166 					if (n + 1 != newsgroups.size()) {
   167 						groups.append(',');
   168 					}
   169 				}
   170 
   171 				StringBuilder strBuf = new StringBuilder();
   172 				strBuf.append("Posting to group ");
   173 				strBuf.append(groups.toString());
   174 				Log.get().info(strBuf.toString());
   175 
   176 				article.setGroup(groups.toString());
   177 				//article.removeHeader(Headers.REPLY_TO);
   178 				//article.removeHeader(Headers.TO);
   179 
   180 				// Write article to database
   181 				if (updateReq) {
   182 					Log.get().info("Updating " + article.getMessageID()
   183 							+ " with additional groups");
   184 					StorageManager.current().delete(article.getMessageID());
   185 					StorageManager.current().addArticle(article);
   186 				} else {
   187 					Log.get().info("Gatewaying " + article.getMessageID() + " to "
   188 							+ article.getHeader(Headers.NEWSGROUPS)[0]);
   189 					StorageManager.current().addArticle(article);
   190 					Stats.getInstance().mailGatewayed(
   191 							article.getHeader(Headers.NEWSGROUPS)[0]);
   192 				}
   193 				posted = true;
   194 			} else {
   195 				StringBuilder buf = new StringBuilder();
   196 				for (Address toa : msg.getAllRecipients()) {
   197 					buf.append(' ');
   198 					buf.append(toa.toString());
   199 				}
   200 				buf.append(" ");
   201 				buf.append(article.getHeader(Headers.LIST_POST)[0]);
   202 				Log.get().warning("No group for" + buf.toString());
   203 			}
   204 			return posted;
   205 		} catch (Exception ex) {
   206 			Log.get().log(Level.WARNING, ex.getLocalizedMessage(), ex);
   207 			return false;
   208 		}
   209 	}
   210 
   211 	/**
   212 	 * Mails a message received through NNTP to the appropriate mailing list.
   213 	 * This method MAY be called several times by PostCommand for the same
   214 	 * article.
   215 	 */
   216 	public static boolean toList(Article article, String group)
   217 			throws IOException, MessagingException, StorageBackendException {
   218 		// Get mailing lists for the group of this article
   219 		List<String> rcptAddresses = StorageManager.current().getListsForGroup(group);
   220 
   221 		if (rcptAddresses == null || rcptAddresses.isEmpty()) {
   222 			StringBuilder strBuf = new StringBuilder();
   223 			strBuf.append("No ML address found for group ");
   224 			strBuf.append(group);
   225 			Log.get().warning(strBuf.toString());
   226 			return false;
   227 		}
   228 
   229 		for (String rcptAddress : rcptAddresses) {
   230 			// Compose message and send it via given SMTP-Host
   231 			String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
   232 			int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
   233 			String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
   234 			String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
   235 			String smtpFrom = Config.inst().get(
   236 					Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
   237 
   238 			// TODO: Make Article cloneable()
   239 			article.getMessageID(); // Make sure an ID is existing
   240 			article.removeHeader(Headers.NEWSGROUPS);
   241 			article.removeHeader(Headers.PATH);
   242 			article.removeHeader(Headers.LINES);
   243 			article.removeHeader(Headers.BYTES);
   244 
   245 			article.setHeader("To", rcptAddress);
   246 			//article.setHeader("Reply-To", listAddress);
   247 
   248 			if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) {
   249 				rewriteSenderAddress(article); // Set the SENDER address
   250 			}
   251 
   252 			SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
   253 			smtpTransport.send(article, smtpFrom, rcptAddress);
   254 			smtpTransport.close();
   255 
   256 			Stats.getInstance().mailGatewayed(group);
   257 			Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
   258 					+ " was delivered to " + rcptAddress + ".");
   259 		}
   260 		return true;
   261 	}
   262 
   263 	/**
   264 	 * Sets the SENDER header of the given MimeMessage. This might be necessary
   265 	 * for moderated groups that does not allow the "normal" FROM sender.
   266 	 * @param msg
   267 	 * @throws javax.mail.MessagingException
   268 	 */
   269 	private static void rewriteSenderAddress(Article msg)
   270 			throws MessagingException {
   271 		String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
   272 
   273 		if (mlAddress != null) {
   274 			msg.setHeader(Headers.SENDER, mlAddress);
   275 		} else {
   276 			throw new MessagingException("Cannot rewrite SENDER header!");
   277 		}
   278 	}
   279 }