Fix for issue #17. Error when posting to mailinglist is now reported back to user as NNTP error.
3 * see AUTHORS for the list of contributors
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.
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.
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/>.
18 package org.sonews.mlgw;
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;
42 * Dispatches messages from mailing list to newsserver or vice versa.
43 * @author Christian Lins
46 public class Dispatcher {
48 static class PasswordAuthenticator extends Authenticator {
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");
57 return new PasswordAuthentication(username, password);
62 * Chunks out the email address of the full List-Post header field.
63 * @param listPostValue
64 * @return The matching email address or null
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());
78 * This method inspects the header of the given message, trying
79 * to find the most appropriate recipient.
81 * @param fallback If this is false only List-Post and X-List-Post headers
83 * @return null or fitting group name for the given message.
85 private static List<String> getGroupFor(final Message msg, final boolean fallback)
86 throws MessagingException, StorageBackendException {
87 List<String> groups = null;
89 // Is there a List-Post header?
90 String[] listPost = msg.getHeader(Headers.LIST_POST);
91 InternetAddress listPostAddr;
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);
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
114 if (toa instanceof InternetAddress) {
115 List<String> g = StorageManager.current().getGroupsForList(((InternetAddress) toa).getAddress());
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.
132 public static boolean toGroup(final Message msg) {
134 throw new IllegalArgumentException("Argument 'msg' must not be null!");
138 // Create new Article object
139 Article article = new Article(msg);
140 boolean posted = false;
142 // Check if this mail is already existing the storage
144 StorageManager.current().isArticleExisting(article.getMessageID());
146 List<String> newsgroups = getGroupFor(msg, !updateReq);
147 List<String> oldgroups = new ArrayList<String>();
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());
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()) {
171 StringBuilder strBuf = new StringBuilder();
172 strBuf.append("Posting to group ");
173 strBuf.append(groups.toString());
174 Log.get().info(strBuf.toString());
176 article.setGroup(groups.toString());
177 //article.removeHeader(Headers.REPLY_TO);
178 //article.removeHeader(Headers.TO);
180 // Write article to database
182 Log.get().info("Updating " + article.getMessageID()
183 + " with additional groups");
184 StorageManager.current().delete(article.getMessageID());
185 StorageManager.current().addArticle(article);
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]);
195 StringBuilder buf = new StringBuilder();
196 for (Address toa : msg.getAllRecipients()) {
198 buf.append(toa.toString());
201 buf.append(article.getHeader(Headers.LIST_POST)[0]);
202 Log.get().warning("No group for" + buf.toString());
205 } catch (Exception ex) {
206 Log.get().log(Level.WARNING, ex.getLocalizedMessage(), ex);
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
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);
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());
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]);
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);
245 article.setHeader("To", rcptAddress);
246 //article.setHeader("Reply-To", listAddress);
248 if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) {
249 rewriteSenderAddress(article); // Set the SENDER address
252 SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
253 smtpTransport.send(article, smtpFrom, rcptAddress);
254 smtpTransport.close();
256 Stats.getInstance().mailGatewayed(group);
257 Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
258 + " was delivered to " + rcptAddress + ".");
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.
267 * @throws javax.mail.MessagingException
269 private static void rewriteSenderAddress(Article msg)
270 throws MessagingException {
271 String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
273 if (mlAddress != null) {
274 msg.setHeader(Headers.SENDER, mlAddress);
276 throw new MessagingException("Cannot rewrite SENDER header!");