HSQLDB backend support completed, but untested.
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/>.
19 package org.sonews.mlgw;
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;
42 * Dispatches messages from mailing list to newsserver or vice versa.
43 * @author Christian Lins
46 public class Dispatcher
49 static class PasswordAuthenticator extends Authenticator
53 public PasswordAuthentication getPasswordAuthentication()
55 final String username =
56 Config.inst().get(Config.MLSEND_USER, "user");
57 final String password =
58 Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
60 return new PasswordAuthentication(username, password);
65 * Chunks out the email address of the full List-Post header field.
66 * @param listPostValue
67 * @return The matching email address or null
69 private static String chunkListPost(String listPostValue)
71 // listPostValue is of form "<mailto:dev@openoffice.org>"
72 Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
73 Matcher mailMatcher = mailPattern.matcher(listPostValue);
74 if (mailMatcher.find()) {
75 return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
82 * This method inspects the header of the given message, trying
83 * to find the most appropriate recipient.
85 * @param fallback If this is false only List-Post and X-List-Post headers
87 * @return null or fitting group name for the given message.
89 private static List<String> getGroupFor(final Message msg, final boolean fallback)
90 throws MessagingException, StorageBackendException
92 List<String> groups = null;
94 // Is there a List-Post header?
95 String[] listPost = msg.getHeader(Headers.LIST_POST);
96 InternetAddress listPostAddr;
98 if (listPost == null || listPost.length == 0 || "".equals(listPost[0])) {
99 // Is there a X-List-Post header?
100 listPost = msg.getHeader(Headers.X_LIST_POST);
103 if (listPost != null && listPost.length > 0
104 && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null) {
105 // listPost[0] is of form "<mailto:dev@openoffice.org>"
106 listPost[0] = chunkListPost(listPost[0]);
107 listPostAddr = new InternetAddress(listPost[0], false);
108 groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
109 } else if (fallback) {
110 Log.get().info("Using fallback recipient discovery for: " + msg.getSubject());
111 groups = new ArrayList<String>();
112 // Fallback to TO/CC/BCC addresses
113 Address[] to = msg.getAllRecipients();
114 for (Address toa : to) // Address can have '<' '>' around
116 if (toa instanceof InternetAddress) {
117 List<String> g = StorageManager.current().getGroupsForList(((InternetAddress) toa).getAddress());
127 * Posts a message that was received from a mailing list to the
128 * appropriate newsgroup.
129 * If the message already exists in the storage, this message checks
130 * if it must be posted in an additional group. This can happen for
131 * crosspostings in different mailing lists.
134 public static boolean toGroup(final Message msg)
137 throw new IllegalArgumentException("Argument 'msg' must not be null!");
141 // Create new Article object
142 Article article = new Article(msg);
143 boolean posted = false;
145 // Check if this mail is already existing the storage
147 StorageManager.current().isArticleExisting(article.getMessageID());
149 List<String> newsgroups = getGroupFor(msg, !updateReq);
150 List<String> oldgroups = new ArrayList<String>();
152 // Check for duplicate entries of the same group
153 Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
154 if (oldArticle != null) {
155 List<Group> oldGroups = oldArticle.getGroups();
156 for (Group oldGroup : oldGroups) {
157 if (!newsgroups.contains(oldGroup.getName())) {
158 oldgroups.add(oldGroup.getName());
164 if (newsgroups.size() > 0) {
165 newsgroups.addAll(oldgroups);
166 StringBuilder groups = new StringBuilder();
167 for (int n = 0; n < newsgroups.size(); n++) {
168 groups.append(newsgroups.get(n));
169 if (n + 1 != newsgroups.size()) {
173 Log.get().info("Posting to group " + groups.toString());
175 article.setGroup(groups.toString());
176 //article.removeHeader(Headers.REPLY_TO);
177 //article.removeHeader(Headers.TO);
179 // Write article to database
181 Log.get().info("Updating " + article.getMessageID()
182 + " with additional groups");
183 StorageManager.current().delete(article.getMessageID());
184 StorageManager.current().addArticle(article);
186 Log.get().info("Gatewaying " + article.getMessageID() + " to "
187 + article.getHeader(Headers.NEWSGROUPS)[0]);
188 StorageManager.current().addArticle(article);
189 Stats.getInstance().mailGatewayed(
190 article.getHeader(Headers.NEWSGROUPS)[0]);
194 StringBuilder buf = new StringBuilder();
195 for (Address toa : msg.getAllRecipients()) {
197 buf.append(toa.toString());
199 buf.append(" " + article.getHeader(Headers.LIST_POST)[0]);
200 Log.get().warning("No group for" + buf.toString());
203 } catch (Exception ex) {
204 ex.printStackTrace();
210 * Mails a message received through NNTP to the appropriate mailing list.
211 * This method MAY be called several times by PostCommand for the same
214 public static void toList(Article article, String group)
215 throws IOException, MessagingException, StorageBackendException
217 // Get mailing lists for the group of this article
218 List<String> rcptAddresses = StorageManager.current().getListsForGroup(group);
220 if (rcptAddresses == null || rcptAddresses.size() == 0) {
221 Log.get().warning("No ML-address for " + group + " found.");
225 for (String rcptAddress : rcptAddresses) {
226 // Compose message and send it via given SMTP-Host
227 String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
228 int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
229 String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
230 String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
231 String smtpFrom = Config.inst().get(
232 Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
234 // TODO: Make Article cloneable()
235 article.getMessageID(); // Make sure an ID is existing
236 article.removeHeader(Headers.NEWSGROUPS);
237 article.removeHeader(Headers.PATH);
238 article.removeHeader(Headers.LINES);
239 article.removeHeader(Headers.BYTES);
241 article.setHeader("To", rcptAddress);
242 //article.setHeader("Reply-To", listAddress);
244 if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) {
245 rewriteSenderAddress(article); // Set the SENDER address
248 SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
249 smtpTransport.send(article, smtpFrom, rcptAddress);
250 smtpTransport.close();
252 Stats.getInstance().mailGatewayed(group);
253 Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
254 + " was delivered to " + rcptAddress + ".");
259 * Sets the SENDER header of the given MimeMessage. This might be necessary
260 * for moderated groups that does not allow the "normal" FROM sender.
262 * @throws javax.mail.MessagingException
264 private static void rewriteSenderAddress(Article msg)
265 throws MessagingException
267 String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
269 if (mlAddress != null) {
270 msg.setHeader(Headers.SENDER, mlAddress);
272 throw new MessagingException("Cannot rewrite SENDER header!");