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 <http://www.gnu.org/licenses/>.
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 "<mailto:dev@openoffice.org>"
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<String> getGroupFor(final Message msg, final boolean fallback)
cli@50: 			throws MessagingException, StorageBackendException {
cli@37: 		List<String> 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 "<mailto:dev@openoffice.org>"
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<String>();
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<String> 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<String> newsgroups = getGroupFor(msg, !updateReq);
cli@37: 			List<String> oldgroups = new ArrayList<String>();
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<Group> 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<String> 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: }