0
|
1 package nabble.model;
|
|
2
|
|
3 import fschmidt.db.DbDatabase;
|
|
4 import fschmidt.db.DbNull;
|
|
5 import fschmidt.util.java.DateUtils;
|
|
6 import fschmidt.util.java.HtmlUtils;
|
|
7 import fschmidt.util.mail.Mail;
|
|
8 import fschmidt.util.mail.MailAddress;
|
|
9 import fschmidt.util.mail.MailAddressException;
|
|
10 import fschmidt.util.mail.MailEncodingException;
|
|
11 import fschmidt.util.mail.MailException;
|
|
12 import fschmidt.util.mail.MailHome;
|
|
13 import fschmidt.util.mail.MailIterator;
|
|
14 import fschmidt.util.mail.MailParseException;
|
|
15 import fschmidt.util.mail.Pop3Server;
|
|
16 import fschmidt.util.mail.javamail.MstorInServer;
|
|
17 import nabble.model.lucene.HitCollector;
|
|
18 import nabble.model.lucene.LuceneSearcher;
|
|
19 import org.apache.lucene.document.Document;
|
|
20 import org.apache.lucene.queryParser.ParseException;
|
|
21 import org.apache.lucene.queryParser.QueryParser;
|
|
22 import org.apache.lucene.search.BooleanQuery;
|
|
23 import org.apache.lucene.search.Filter;
|
|
24 import org.apache.lucene.search.Query;
|
|
25 import org.apache.lucene.util.Version;
|
|
26 import org.slf4j.Logger;
|
|
27 import org.slf4j.LoggerFactory;
|
|
28
|
|
29 import java.io.File;
|
|
30 import java.io.IOException;
|
|
31 import java.io.PrintWriter;
|
|
32 import java.io.StringWriter;
|
|
33 import java.io.UnsupportedEncodingException;
|
|
34 import java.sql.Connection;
|
|
35 import java.sql.PreparedStatement;
|
|
36 import java.sql.ResultSet;
|
|
37 import java.sql.SQLException;
|
|
38 import java.text.DateFormat;
|
|
39 import java.text.SimpleDateFormat;
|
|
40 import java.util.ArrayList;
|
|
41 import java.util.Arrays;
|
|
42 import java.util.Collection;
|
|
43 import java.util.Date;
|
|
44 import java.util.HashSet;
|
|
45 import java.util.List;
|
|
46 import java.util.Set;
|
|
47 import java.util.TimeZone;
|
|
48 import java.util.concurrent.TimeUnit;
|
|
49 import java.util.regex.Matcher;
|
|
50 import java.util.regex.Pattern;
|
|
51 import javax.mail.internet.InternetAddress;
|
|
52 import javax.mail.internet.AddressException;
|
|
53
|
|
54
|
|
55 final class MailingLists {
|
|
56 private static final Logger logger = LoggerFactory.getLogger(MailingLists.class);
|
|
57
|
|
58 private static final float nameChangeFreq = Init.get("mlNameChangeFreq",0.1f);
|
|
59 static final Pop3Server pop3Server = (Pop3Server)Init.get("mailingListArchivePop3Server");
|
|
60
|
|
61 private MailingLists() {} // never
|
|
62
|
|
63 static {
|
|
64 if( Init.hasDaemons ) {
|
|
65 runMailingLists();
|
|
66 }
|
|
67 }
|
|
68
|
|
69 private static void runMailingLists() {
|
|
70 if( pop3Server == null ) {
|
|
71 logger.warn("no pop3 server defined, mailing lists not running");
|
|
72 return;
|
|
73 }
|
|
74 Executors.scheduleWithFixedDelay(new Runnable() {
|
|
75 public void run(){
|
|
76 try {
|
|
77 processMail();
|
|
78 processFwds();
|
|
79 } catch(MailException e) {
|
|
80 logger.error("mailing list processing",e);
|
|
81 }
|
|
82 }
|
|
83 }, 10, 10, TimeUnit.SECONDS );
|
|
84 logger.info("mailing lists enabled");
|
|
85 }
|
|
86
|
|
87 private static void processMail() {
|
|
88 MailIterator mails = pop3Server.getMail();
|
|
89 int count = 0;
|
|
90 try {
|
|
91 while( mails.hasNext() ) {
|
|
92 Mail mail = mails.next();
|
|
93 try {
|
|
94 makePost(mail);
|
|
95 count++;
|
|
96 } catch (MailAddressException e) {
|
|
97 logger.warn("mail:\n"+mail.getRawInput(),e); // screwed-up mail
|
|
98 } catch (Exception e) {
|
|
99 logger.error("mail:\n"+mail.getRawInput(),e);
|
|
100 }
|
|
101 }
|
|
102 } finally {
|
|
103 mails.close();
|
|
104 if( count > 0 )
|
|
105 logger.error("Processed " + count + " emails.");
|
|
106 }
|
|
107 }
|
|
108
|
|
109 static MailingList.ImportResult importMbox(File file,MailingListImpl ml,String mailErrorsToS,int maxErrors)
|
|
110 throws ModelException
|
|
111 {
|
|
112 final DateFormat mailmanDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy");
|
|
113 final DateFormat mailDateFormat = new javax.mail.internet.MailDateFormat();
|
|
114 mailmanDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
|
115 mailDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
|
116 MailAddress mailErrorsTo = new MailAddress(mailErrorsToS);
|
|
117 MstorInServer server = new MstorInServer(file);
|
|
118 server.setMetaEnabled(false);
|
|
119 MailIterator mails = server.getMail();
|
|
120 try {
|
|
121 int imported = 0;
|
|
122 int errors = 0;
|
|
123 while( mails.hasNext() ) {
|
|
124 Mail mail = mails.next();
|
|
125 try {
|
|
126 try {
|
|
127 mail.getFrom();
|
|
128 } catch (MailAddressException e) {
|
|
129 String[] from = mail.getHeader("From");
|
|
130 if (from == null || from.length == 0)
|
|
131 throw new MailAddressException("'From' not found in the header", e);
|
|
132 mail.setHeader("From", from[0].replace(" at ", "@"));
|
|
133 }
|
|
134 Date sentDate = mail.getSentDate();
|
|
135 if ((sentDate==null || sentDate.getTime() < 0) && mail.getHeader("Date")!=null) {
|
|
136 String dateH = mail.getHeader("Date")[0];
|
|
137 if (dateH!=null) {
|
|
138 try {
|
|
139 sentDate = mailmanDateFormat.parse(dateH);
|
|
140 } catch (java.text.ParseException e) {}
|
|
141 if (sentDate!=null)
|
|
142 mail.setSentDate(sentDate);
|
|
143 }
|
|
144 }
|
|
145 if ((sentDate==null || sentDate.getTime() < 0) && mail.getHeader("Resent-date")!=null) {
|
|
146 String dateH = mail.getHeader("Resent-date")[0];
|
|
147 if (dateH!=null) {
|
|
148 try {
|
|
149 sentDate = mailDateFormat.parse(dateH);
|
|
150 } catch (java.text.ParseException e) {}
|
|
151 if (sentDate!=null)
|
|
152 mail.setSentDate(sentDate);
|
|
153 }
|
|
154 }
|
|
155 if ((sentDate==null || sentDate.getTime() < 0)) {
|
|
156 String rawInput = mail.getRawInput();
|
|
157 try {
|
|
158 String dateH = rawInput.substring(rawInput.indexOf(' ',5), rawInput.indexOf('\n')).trim();
|
|
159 sentDate = mailmanDateFormat.parse(dateH);
|
|
160 } catch (Exception e) {
|
|
161 logger.error("",e); // what kind of exception is ok?
|
|
162 }
|
|
163 if (sentDate!=null)
|
|
164 mail.setSentDate(sentDate);
|
|
165 }
|
|
166 makeForumPost(mail,ml,true);
|
|
167 imported++;
|
|
168 } catch (Exception e) {
|
|
169 sendErrorMail(mail, e, mailErrorsTo);
|
|
170 errors++;
|
|
171 if( errors >= maxErrors )
|
|
172 throw ModelException.newInstance("import_mbox_errors",""+errors+" errors reached after importing "+imported+" messages");
|
|
173 }
|
|
174 }
|
|
175 final int imported2 = imported;
|
|
176 final int errors2 = errors;
|
|
177 return new MailingList.ImportResult() {
|
|
178 public int getImported() { return imported2; }
|
|
179 public int getErrors() { return errors2; }
|
|
180 };
|
|
181 } finally {
|
|
182 mails.close();
|
|
183 }
|
|
184 }
|
|
185
|
|
186 private static void makePost(Mail mail)
|
|
187 throws ModelException
|
|
188 {
|
|
189 MailingListImpl ml = getMailingList(mail);
|
|
190 if (ml == null) {
|
|
191 logger.info("Mailing list not found for: " + Arrays.asList(mail.getTo()));
|
|
192 return;
|
|
193 }
|
|
194 if (checkForward(mail, ml)) {
|
|
195 return;
|
|
196 }
|
|
197 if (checkPending(mail, ml)) {
|
|
198 return;
|
|
199 }
|
|
200 makeForumPost(mail, ml, false);
|
|
201 }
|
|
202
|
|
203 private static void makeForumPost(Mail mail, MailingListImpl ml, boolean isImport)
|
|
204 throws ModelException
|
|
205 {
|
|
206 String messageID = getMessageID(mail, msgFmt);
|
|
207 mail.setMessageID(messageID);
|
|
208
|
|
209 String message = mail.getRawInput();
|
|
210 message = message.replace("\000",""); // postgres can't handle 0
|
|
211 if( !msgFmt.isOk(message) )
|
|
212 return;
|
|
213 String text = msgFmt.getMailText(message,null);
|
|
214 NodeImpl forum = ml.getForumImpl();
|
|
215
|
|
216 if( doNotArchive(text) || (doNotArchive(mail) && !ml.ignoreNoArchive()) ) {
|
|
217 logger.info("XNoArchive in "+forum.getSubject());
|
|
218 return;
|
|
219 }
|
|
220
|
|
221 DbDatabase db;
|
|
222 try {
|
|
223 db = forum.siteKey.getDb();
|
|
224 } catch(UpdatingException e) {
|
|
225 return; // hack for schema migration
|
|
226 }
|
|
227 db.beginTransaction();
|
|
228 try {
|
|
229 forum = (NodeImpl)forum.getGoodCopy();
|
|
230 MailingListImpl mailingList = forum.getMailingListImpl();
|
|
231
|
|
232 {
|
|
233 NodeImpl post = forum.getNodeImplFromMessageID(messageID);
|
|
234 if( post != null) {
|
|
235 if(isImport)
|
|
236 return;
|
|
237 throw new RuntimeException("MessageID "+messageID+" already in db for forum "+forum.getId());
|
|
238 }
|
|
239 }
|
|
240
|
|
241 UserImpl user = getUser(mail, mailingList);
|
|
242 if (user.isNoArchive())
|
|
243 return;
|
|
244
|
|
245 String subject = mailingList.fixSubject(mail.getSubject());
|
|
246 if( subject==null || subject.trim().equals("") )
|
|
247 subject = "(no subject)";
|
|
248
|
|
249 if (!isImport) {
|
|
250 ListServer oldListServer = mailingList.getListServer();
|
|
251 if (oldListServer==ListServer.unknown || oldListServer instanceof ListServer.Mailman) {
|
|
252 ListServer listServer = detectListServer(mail);
|
|
253 if (listServer!=null && listServer!=oldListServer && (oldListServer==ListServer.unknown || listServer==ListServer.mailman21)) {
|
|
254 mailingList.setListServer(listServer);
|
|
255 mailingList.update();
|
|
256 }
|
|
257 }
|
|
258 }
|
|
259
|
|
260 Date now = new Date();
|
|
261 Date date = mail.getSentDate();
|
|
262 if( date==null || date.compareTo(now) > 0 || date.getTime() < 0)
|
|
263 date = now;
|
|
264
|
|
265 boolean isGuessedParent = false;
|
|
266 String parentID = getParentID(mail, messageID);
|
|
267 NodeImpl parent = forum.getNodeImplFromMessageID(parentID);
|
|
268 if ( parent!=null && threadBySubject(forum, subject, parent.getSubject()) ) {
|
|
269 parent = null;
|
|
270 }
|
|
271
|
|
272 NodeImpl[] orphans = NodeImpl.getFromParentID(messageID,mailingList);
|
|
273 if ( parent==null ) {
|
|
274 try {
|
|
275 parent = guessParent(mail, date, mailingList, subject, orphans);
|
|
276 if ( parent != null )
|
|
277 isGuessedParent = true;
|
|
278 } catch(IOException e) {
|
|
279 logger.error("guessParent failed",e);
|
|
280 }
|
|
281 }
|
|
282
|
|
283 NodeImpl post = NodeImpl.newChildNode(Node.Kind.POST,user,subject,message,msgFmt,parent==null?forum:parent);
|
|
284 if( parent==null && parentID != null ) {
|
|
285 logger.debug("Orphan "+messageID+" starting new thread ");
|
|
286 isGuessedParent = true;
|
|
287 }
|
|
288
|
|
289 post.setWhenCreated(date);
|
|
290 post.setMessageID(messageID);
|
|
291 if (isGuessedParent) {
|
|
292 post.setGuessedParent(parentID);
|
|
293 } else if (parent==null) {
|
|
294 // for root posts which do not have parentID set guess flag to uncertain
|
|
295 post.setGuessedParent((Boolean) null);
|
|
296 }
|
|
297 post.insert(false);
|
|
298 if( isGuessedParent && parentID==null )
|
|
299 logger.debug("no parentID for "+post);
|
|
300
|
|
301 for (NodeImpl orphan : orphans) {
|
|
302 try {
|
|
303 if (!threadBySubject(forum, subject, orphan.getSubject())) {
|
|
304 orphan.changeParentImpl(post);
|
|
305 }
|
|
306 } catch (ModelException.NodeLoop e) {
|
|
307 logger.error("", e); // should not happen now...
|
|
308 orphan.getDbRecord().fields().put("parent_message_id", DbNull.STRING);
|
|
309 orphan.getDbRecord().update();
|
|
310 }
|
|
311 }
|
|
312
|
|
313 db.commitTransaction();
|
|
314 } finally {
|
|
315 db.endTransaction();
|
|
316 }
|
|
317 }
|
|
318
|
|
319 private static boolean threadBySubject(Node forum, String subject, String parentSubject) {
|
|
320 if (!forumsThreadedBySubject.contains(forum.getId())) return false;
|
|
321 return ! normalizeSubject(subject).equals(normalizeSubject(parentSubject));
|
|
322 }
|
|
323
|
|
324 static final Set<Long> forumsThreadedBySubject = new HashSet<Long>(Arrays.asList((Long[])Init.get("forumsThreadedBySubject", new Long[0])));
|
|
325
|
|
326
|
|
327 private static void sendErrorMail(Mail mail, Exception e, MailAddress mailTo) {
|
|
328 if( e instanceof UnsupportedEncodingException
|
|
329 || e instanceof MailAddressException
|
|
330 || e instanceof MailEncodingException
|
|
331 || e instanceof MailParseException
|
|
332 ) {
|
|
333 logger.info(e.toString());
|
|
334 } else {
|
|
335 logger.error("",e);
|
|
336 }
|
|
337 StringWriter sb = new StringWriter();
|
|
338 PrintWriter out = new PrintWriter(sb);
|
|
339 e.printStackTrace( out );
|
|
340 out.close();
|
|
341 String msg = e.getMessage();
|
|
342 if (msg!=null && msg.indexOf('\n')>=0) msg = msg.substring(0, msg.indexOf('\n')).trim();
|
|
343 String subject = "error: "+msg;
|
|
344 MailSubsystem.sendErrorMail(mail, mailTo, subject, sb.toString());
|
|
345 }
|
|
346
|
|
347 private static String getParentID(Mail mail, String messageID) {
|
|
348 String[] inReplyTos = mail.getHeader("In-Reply-To");
|
|
349 if( inReplyTos == null ) {
|
|
350 inReplyTos = mail.getHeader("In-Reply-to");
|
|
351 if( inReplyTos != null )
|
|
352 logger.error("does this happen - case sensitive");
|
|
353 }
|
|
354 if( inReplyTos != null ) {
|
|
355 for (String inReplyTo : inReplyTos) {
|
|
356 for( String s : MailSubsystem.stripMultiBrackets(inReplyTo) ) {
|
|
357 if (!s.equals(messageID) && !s.equals("")) return s;
|
|
358 }
|
|
359 }
|
|
360 }
|
|
361 try {
|
|
362 String[] refs = mail.getHeader("References");
|
|
363 if( refs != null ) {
|
|
364 for (String ref : refs) {
|
|
365 List<String> list = MailSubsystem.stripMultiBrackets(ref);
|
|
366 if( list.isEmpty() )
|
|
367 continue;
|
|
368 String s = list.get(list.size()-1);
|
|
369 if (!s.equals(messageID) && !s.equals("")) return s;
|
|
370 }
|
|
371 }
|
|
372 } catch(MailParseException e) {
|
|
373 logger.warn("screwed up References for messageID="+messageID,e);
|
|
374 }
|
|
375 return null;
|
|
376 }
|
|
377
|
|
378 private static NodeImpl guessParent(Mail mail, Date date, MailingListImpl mailingList, String subject, NodeImpl[] orphans) throws IOException {
|
|
379 Set<NodeImpl> offspring = new HashSet<NodeImpl>();
|
|
380 for (NodeImpl orphan : orphans) {
|
|
381 for (NodeImpl n : orphan.getDescendantImpls()) {
|
|
382 offspring.add(n);
|
|
383 }
|
|
384 }
|
|
385 return guessParent(mail, date, mailingList, subject, offspring);
|
|
386 }
|
|
387
|
|
388 static NodeImpl guessParent(NodeImpl post, Collection<NodeImpl> ignore) {
|
|
389 Mail mail = MailHome.newMail(post.getMessage().getRaw());
|
|
390 NodeImpl forum = post.getAppImpl();
|
|
391 if (forum == null) {
|
|
392 return null; // detached post
|
|
393 }
|
|
394 MailingListImpl mailingList = forum.getAssociatedMailingListImpl();
|
|
395 if (mailingList == null) {
|
|
396 return null; // forum no longer a mailing list
|
|
397 }
|
|
398 try {
|
|
399 Set<NodeImpl> ignoreSet = new HashSet<NodeImpl>();
|
|
400 for( NodeImpl n : post.getDescendantImpls() ) {
|
|
401 ignoreSet.add(n);
|
|
402 }
|
|
403 if (ignore != null) {
|
|
404 ignoreSet.addAll(ignore);
|
|
405 }
|
|
406 return guessParent(mail, post.getWhenCreated(), mailingList, post.getSubject(), ignoreSet);
|
|
407 } catch (IOException e) {
|
|
408 throw new RuntimeException(e);
|
|
409 }
|
|
410 }
|
|
411
|
|
412 private static NodeImpl guessParent(Mail mail, Date date, MailingListImpl mailingList, String subject, Set<NodeImpl> offspring)
|
|
413 throws IOException {
|
|
414 // attach to ancestor if any
|
|
415 NodeImpl forum = mailingList.getForumImpl();
|
|
416 try {
|
|
417 String[] refs = mail.getHeader("References");
|
|
418 if( refs != null ) {
|
|
419 for( String ref : refs ) {
|
|
420 final List<String> list = MailSubsystem.stripMultiBrackets(ref);
|
|
421 for( int i=list.size()-1; i>=0; i-- ) {
|
|
422 String ancestorID = list.get(i);
|
|
423 NodeImpl parent = forum.getNodeImplFromMessageID(ancestorID);
|
|
424 if (parent!=null && !offspring.contains(parent) && !threadBySubject(forum,subject,parent.getSubject())) {
|
|
425 logger.debug("Attaching orphan "+mail.getMessageID()+" to grandparent "+parent);
|
|
426 return parent;
|
|
427 }
|
|
428 }
|
|
429 }
|
|
430 }
|
|
431 } catch(MailParseException e) {
|
|
432 logger.warn("screwed up References",e);
|
|
433 }
|
|
434 // handle lost In-Reply-To headers
|
|
435 // heuristics - use Thread-Topic header to find matching subjects in last 3 days
|
|
436 String[] threadTopics = mail.getHeader("Thread-Topic");
|
|
437 String threadTopic = threadTopics==null?null:mailingList.fixSubject(threadTopics[0]);
|
|
438 long forumId = forum.getId();
|
|
439 Filter filter = Lucene.getRangeFilter(DateUtils.addDays(date, -7), date);
|
|
440 SiteImpl site = forum.getSiteImpl();
|
|
441 LuceneSearcher searcher = Lucene.newSearcher(site);
|
|
442 try {
|
|
443 if( threadTopic != null ) {
|
|
444 threadTopic = threadTopic.toLowerCase();
|
|
445 if (threadTopic.startsWith("re: "))
|
|
446 threadTopic = threadTopic.substring(4);
|
|
447 threadTopic = threadTopic.trim();
|
|
448 if (!threadTopic.equals("")) {
|
|
449 NodeImpl parent = (NodeImpl)getPriorPost(site,searcher, forumId, threadTopic, filter, date, offspring);
|
|
450 if( parent!=null && !offspring.contains(parent) && !threadBySubject(forum,subject,parent.getSubject())) return parent;
|
|
451 }
|
|
452 }
|
|
453 // if no thread-topic, but subject starts with Re:, try with subject
|
|
454 subject = subject.toLowerCase();
|
|
455 if( subject.startsWith("re: ") ) {
|
|
456 subject = subject.substring(4).trim();
|
|
457 if ( !subject.equals(threadTopic) && !"".equals(subject) ) {
|
|
458 NodeImpl parent = (NodeImpl)getPriorPost(site,searcher, forumId, subject, filter, date, offspring);
|
|
459 if( parent!=null && !offspring.contains(parent)) return parent;
|
|
460 }
|
|
461 }
|
|
462 } finally {
|
|
463 searcher.close();
|
|
464 }
|
|
465 return null;
|
|
466 }
|
|
467
|
|
468 private static boolean checkPending(Mail mail, MailingListImpl ml) {
|
|
469 if (checkPending(getMessageID(mail, msgFmt), ml)) {
|
|
470 return true;
|
|
471 }
|
|
472 String[] xMessageId = mail.getHeader("X-Message-Id");
|
|
473 if (xMessageId != null && xMessageId.length > 0 && checkPending(MailSubsystem.stripBrackets(xMessageId[0]), ml)) {
|
|
474 return true;
|
|
475 }
|
|
476 xMessageId = mail.getHeader("X-Original-Message-Id");
|
|
477 if (xMessageId != null && xMessageId.length > 0 && checkPending(MailSubsystem.stripBrackets(xMessageId[0]), ml)) {
|
|
478 return true;
|
|
479 }
|
|
480 return false;
|
|
481 }
|
|
482
|
|
483 private static boolean checkPending(String messageID, MailingListImpl ml) {
|
|
484 NodeImpl pendingPost = ml.getForumImpl().getNodeImplFromMessageID(messageID);
|
|
485 if( pendingPost==null )
|
|
486 return false;
|
|
487 Node.MailToList mail = pendingPost.getMailToList();
|
|
488 if( mail == null ) {
|
|
489 logger.warn("MessageID "+messageID+" already in db as "+pendingPost+" for forum "+ml.getId());
|
|
490 } else if( !mail.isPending() ) {
|
|
491 logger.error("post not pending "+pendingPost);
|
|
492 } else {
|
|
493 mail.clearPending();
|
|
494 }
|
|
495 return true;
|
|
496 }
|
|
497
|
|
498 private static boolean doNotArchive(Mail mail) {
|
|
499 String[] xNoArchive = mail.getHeader("X-No-Archive");
|
|
500 if (xNoArchive != null && xNoArchive.length > 0 && "yes".equalsIgnoreCase(xNoArchive[0])) {
|
|
501 return true;
|
|
502 }
|
|
503 String[] xArchive = mail.getHeader("X-Archive");
|
|
504 if (xArchive != null && xArchive.length > 0 && xArchive[0] != null && (xArchive[0].startsWith("expiry") || "no".equalsIgnoreCase(xArchive[0]))) {
|
|
505 return true;
|
|
506 }
|
|
507 String[] archive = mail.getHeader("Archive");
|
|
508 if (archive != null && archive.length > 0 && "no".equalsIgnoreCase(archive[0])) {
|
|
509 return true;
|
|
510 }
|
|
511 return false;
|
|
512 }
|
|
513
|
|
514 private static final Pattern xNoArchivePtn = Pattern.compile("(?im)\\AX-No-Archive: yes *$");
|
|
515 private static boolean doNotArchive(String text) {
|
|
516 return xNoArchivePtn.matcher(text).find();
|
|
517 }
|
|
518
|
|
519 private static MailingListImpl getMailingList(Mail mail) {
|
|
520 MailingListImpl ml = null;
|
|
521 String[] a = mail.getHeader("Envelope-To");
|
|
522 if (a == null)
|
|
523 a = mail.getHeader("X-Delivered-to"); // fastmail
|
|
524 if (a == null)
|
|
525 a = mail.getHeader("X-Original-To"); // postfix
|
|
526 if( a.length > 1 )
|
|
527 a = new String[] { a[0] };
|
|
528 for( String address : a[0].split(",") ) {
|
|
529 address = address.trim();
|
|
530 MailingListImpl candidate = MailingListImpl.getMailingListByEnvelopeAddress(address);
|
|
531 if (candidate == null) {
|
|
532 // escaped list mail, bounce mail
|
|
533 String returnPath = MailSubsystem.getReturnPath(mail);
|
|
534 if( returnPath.equals(address) )
|
|
535 continue; // ignore spam
|
|
536 MailSubsystem.bounce(mail,
|
|
537 "Delivery to the following recipient failed permanently:\n\n "
|
|
538 + address
|
|
539 + "\n\nNo archive exists for this address.\n"
|
|
540 );
|
|
541 logger.warn( "no mailing list found for "+address+" - bouncing mail to "+returnPath + ":\n" + mail);
|
|
542 } else {
|
|
543 if( ml != null )
|
|
544 logger.error("mailing list already set");
|
|
545 ml = candidate;
|
|
546 }
|
|
547 }
|
|
548 return ml;
|
|
549 }
|
|
550
|
|
551 private static String extractDomain(String email) {
|
|
552 String domain = email.substring(email.indexOf('@')+1).toLowerCase();
|
|
553 // hack to unify google messages
|
|
554 return domain.replace("google.com","googlegroups.com");
|
|
555 }
|
|
556
|
|
557 private static boolean checkForward(Mail mail, MailingListImpl ml) {
|
|
558 // check if the archive guessed from subscription address is not presented in the
|
|
559 // common headers that contain list address, forward to the archive owner in this case
|
|
560 String envTo[] = mail.getHeader("Envelope-To");
|
|
561 if (envTo == null)
|
|
562 envTo = mail.getHeader("X-Delivered-to"); // fastmail
|
|
563 if (envTo == null)
|
|
564 envTo = mail.getHeader("X-Original-To"); // postfix
|
|
565 String originalTo = envTo[0];
|
|
566 {
|
|
567 MailAddress[] to = mail.getTo();
|
|
568 if( to==null || to.length!=1 || !to[0].getAddrSpec().equalsIgnoreCase(originalTo) )
|
|
569 return false;
|
|
570 }
|
|
571 // check for domain of the message's From: or Reply-To:
|
|
572 String listAddress = ml.getListAddress();
|
|
573 String domain = extractDomain(listAddress);
|
|
574 String maintenanceMessageReplyTo = null;
|
|
575 {
|
|
576 MailAddress[] replyTos = mail.getReplyTo();
|
|
577 if (replyTos != null) {
|
|
578 for (MailAddress replyTo : replyTos) {
|
|
579 String replyDomain = extractDomain(replyTo.getAddrSpec());
|
|
580 if (replyDomain.endsWith(domain) || domain.endsWith(replyDomain)) {
|
|
581 maintenanceMessageReplyTo = replyTo.getAddrSpec();
|
|
582 break;
|
|
583 }
|
|
584 }
|
|
585 }
|
|
586 }
|
|
587 MailAddress from = mail.getFrom();
|
|
588 // first we compare the domains
|
|
589 if( maintenanceMessageReplyTo == null && from != null && (extractDomain(from.getAddrSpec()).endsWith(domain) || domain.endsWith(extractDomain(from.getAddrSpec()))))
|
|
590 maintenanceMessageReplyTo = from.getAddrSpec();
|
|
591 // check if this is a majordomo email
|
|
592 if (maintenanceMessageReplyTo == null && from != null && from.getAddrSpec().toLowerCase().startsWith("majordomo@"))
|
|
593 maintenanceMessageReplyTo = from.getAddrSpec();
|
|
594
|
|
595 if( maintenanceMessageReplyTo != null ) {
|
|
596 mail.setReplyTo( new MailAddress(fwdEmail(originalTo, maintenanceMessageReplyTo) ));
|
|
597 MailAddress ownerAddress = getArchiveOwnerAddress(ml);
|
|
598 MailHome.getDefaultSmtpServer().send(mail,ownerAddress);
|
|
599 logger.info("Forwarding maintenance message to owner: " + ownerAddress);
|
|
600 logger.info(mail.getRawInput());
|
|
601 return true;
|
|
602 }
|
|
603 if( MailSubsystem.getReturnPath(mail).equals("") ) {
|
|
604 MailAddress ownerAddress = getArchiveOwnerAddress(ml);
|
|
605 MailHome.getDefaultSmtpServer().send(mail,ownerAddress);
|
|
606 logger.info("Forwarding maintenance message to owner: " + ownerAddress);
|
|
607 logger.info(mail.getRawInput());
|
|
608 return true;
|
|
609 }
|
|
610 logger.info("Bouncing email to: " + MailSubsystem.getReturnPath(mail) + " / envelopeTo = " + originalTo + "\n" + mail.getRawInput());
|
|
611 MailSubsystem.bounce(mail,
|
|
612 "Delivery to the following recipient failed permanently:\n\n "
|
|
613 + originalTo
|
|
614 + "\n\nThis email address is only for archiving mailing lists and should not be used directly.\n"
|
|
615 );
|
|
616 return true;
|
|
617 }
|
|
618
|
|
619 private static MailAddress getArchiveOwnerAddress(MailingListImpl mailingList) {
|
|
620 // If this list was exported to another server, we have to send this email
|
|
621 // to the person that did the export. Otherwise we send to the current owner.
|
|
622 String exportOwner = mailingList.getExportOwner();
|
|
623 if (exportOwner == null) {
|
|
624 // Send to the current owner...
|
|
625 User owner = mailingList.getForumImpl().getOwnerImpl();
|
|
626 return new MailAddress(owner.getEmail(), owner.getName());
|
|
627 } else {
|
|
628 // Send to the person who exported the archive...
|
|
629 return new MailAddress(exportOwner);
|
|
630 }
|
|
631 }
|
|
632
|
|
633 private static MailAddress toMailAddress(String s) {
|
|
634 try {
|
|
635 InternetAddress ia = new InternetAddress(s);
|
|
636 return new MailAddress(ia.getAddress(),ia.getPersonal());
|
|
637 } catch(AddressException e) {
|
|
638 return null;
|
|
639 }
|
|
640 }
|
|
641
|
|
642 private static UserImpl getUser(Mail mail, MailingListImpl mailingList) {
|
|
643 MailAddress addr = null;
|
|
644 String a[] = mail.getHeader("X-Original-From");
|
|
645 if( a != null )
|
|
646 addr = toMailAddress(a[0]);
|
|
647 if( addr == null )
|
|
648 addr = mail.getFrom();
|
|
649 String email = addr.getAddrSpec();
|
|
650 if (email == null || "".equals(email.trim()))
|
|
651 {
|
|
652 throw new MailAddressException("Invalid sender address: "+addr);
|
|
653 }
|
|
654 SiteImpl site = mailingList.getForumImpl().getSiteImpl();
|
|
655 UserImpl user = site.getUserImplFromEmail(email);
|
|
656 if( user==null || !user.isRegistered() ) {
|
|
657 String username;
|
|
658 if( email.equalsIgnoreCase(mailingList.getListAddress()) ) {
|
|
659 username = mailingList.getForum().getSubject() + " mailing list";
|
|
660 } else {
|
|
661 username = addr.getDisplayName();
|
|
662 if( username == null || "".equals(username.trim()) ) {
|
|
663 username = email.indexOf('@')>0 ? email.substring(0, email.indexOf('@')) : email;
|
|
664 }
|
|
665 }
|
|
666 if( username.endsWith(" (JIRA)") ) {
|
|
667 username = "JIRA "+email;
|
|
668 }
|
|
669 if( user==null ) {
|
|
670 user = UserImpl.createGhost(site,email);
|
|
671 user.setNameLike(username,false);
|
|
672 user.insert();
|
|
673 } else {
|
|
674 String oldName = user.getName();
|
|
675 if( !oldName.toLowerCase().startsWith(username.toLowerCase())
|
|
676 && (Math.random() < nameChangeFreq)
|
|
677 ) {
|
|
678 user.setNameLike(username,false);
|
|
679 user.getDbRecord().update();
|
|
680 logger.warn("changed name of "+user+" from '"+oldName+"' to '"+user.getName()+"'");
|
|
681 }
|
|
682 }
|
|
683 }
|
|
684 return user;
|
|
685 }
|
|
686
|
|
687 static final MailMessageFormat msgFmt = new MailMessageFormat('m', "mail");
|
|
688
|
|
689 private static Node getPriorPost(final SiteImpl site,final LuceneSearcher searcher, long forumId, final String subject, Filter filter, final Date to, final Set offspring) throws IOException {
|
|
690 //String phrase = "\""+QueryParser.escape(subject.replace('\"',' '))+"\"";
|
|
691 try {
|
|
692 NodeSearcher.Builder query = new NodeSearcher.Builder(site,forumId);
|
|
693 query.addNodeKind(Node.Kind.POST);
|
|
694 QueryParser parser = new QueryParser(Version.LUCENE_CURRENT,Lucene.SUBJECT_FLD, Lucene.analyzer);
|
|
695 parser.setDefaultOperator(QueryParser.AND_OPERATOR);
|
|
696 Query subjectQuery = parser.parse(QueryParser.escape(subject.replace('\"',' ').replace("&&"," ")));
|
|
697 if (! (subjectQuery instanceof BooleanQuery && ((BooleanQuery)subjectQuery).getClauses().length==0) )
|
|
698 query.addQuery(subjectQuery);
|
|
699 final Node[] resultHolder = new Node[1];
|
|
700 searcher.search( query.build().getQuery(), filter, new HitCollector() {
|
|
701 protected void process(Document doc) {
|
|
702 NodeImpl post = Lucene.node(site,doc);
|
|
703 if (post==null)
|
|
704 return;
|
|
705 String parentSubject = post.getSubject().toLowerCase();
|
|
706 if ( (parentSubject.equals(subject) || (parentSubject.startsWith("re: ") && parentSubject.substring(4).trim().equals(subject)))
|
|
707 && to.after(post.getWhenCreated())
|
|
708 && (resultHolder[0]==null || resultHolder[0].getWhenCreated().before(post.getWhenCreated()))
|
|
709 && !offspring.contains(post)
|
|
710 )
|
|
711 resultHolder[0] = post;
|
|
712 }
|
|
713 });
|
|
714 Node result = resultHolder[0];
|
|
715
|
|
716 if (result != null) {
|
|
717 // find the uppermost post with almost-the-same subject
|
|
718 String subjectEtalon = normalizeSubject(subject.toLowerCase());
|
|
719 Node resultCandidate = result.getTopic();
|
|
720 while (result != null) {
|
|
721 String resultSubject = normalizeSubject(result.getSubject().toLowerCase());
|
|
722 if (!resultSubject.equals(subjectEtalon)) break; // break when subject really changes
|
|
723
|
|
724 // this post has almost-the-same subject
|
|
725 if (!offspring.contains(result)) {
|
|
726 // set only if this node is not presented in escape-set
|
|
727 resultCandidate = result;
|
|
728 }
|
|
729 result = result.getParent();
|
|
730 }
|
|
731 result = resultCandidate;
|
|
732 }
|
|
733 return result;
|
|
734 } catch (ParseException e) {
|
|
735 throw new RuntimeException(e);
|
|
736 }
|
|
737 }
|
|
738
|
|
739 private static final Pattern bracketRegex = Pattern.compile("\\[[^\\[]+\\]");
|
|
740
|
|
741 static Pattern prefixRegex(String prefixes) {
|
|
742 return Pattern.compile( "^((" + prefixes + "): *)+" );
|
|
743 }
|
|
744
|
|
745 private static final Pattern defaultPrefixRegex = prefixRegex("re|aw|res|fwd|答复");
|
|
746
|
|
747 /**
|
|
748 * Remove from subject all possible prefixes which are added while forwarding, replying, etc...
|
|
749 *
|
|
750 * @param subject original subject
|
|
751 * @return normalized subject
|
|
752 */
|
|
753 private static String normalizeSubject(String subject) {
|
|
754 return normalizeSubject(subject,defaultPrefixRegex);
|
|
755 }
|
|
756
|
|
757 static String normalizeSubject(String subject,Pattern prefixRegex) {
|
|
758 if (subject != null) {
|
|
759 subject = subject.toLowerCase().trim();
|
|
760 subject = bracketRegex.matcher(subject).replaceAll("");
|
|
761 subject = prefixRegex.matcher(subject).replaceAll("");
|
|
762 }
|
|
763 return subject;
|
|
764 }
|
|
765
|
|
766 static void nop() {}
|
|
767
|
|
768
|
|
769 private static ListServer detectListServer(Mail mail) {
|
|
770 String[] mailman = mail.getHeader("X-Mailman-Version");
|
|
771 if (mailman!=null && mailman.length==1 && mailman[0]!=null) {
|
|
772 if (mailman[0].startsWith("2.0")) {
|
|
773 return ListServer.mailman20;
|
|
774 } else if (mailman[0].startsWith("2.1")) {
|
|
775 return ListServer.mailman21;
|
|
776 } else if (mailman[0].startsWith("2.")) {
|
|
777 logger.error("unknown mailman version: "+mailman[0]+" in message "+mail.getMessageID());
|
|
778 }
|
|
779 return null;
|
|
780 }
|
|
781
|
|
782 String[] mList = mail.getHeader("Mailing-List");
|
|
783 if (mList!=null && mList.length==1 && mList[0]!=null) {
|
|
784 if (mList[0].indexOf("run by ezmlm")>=0) {
|
|
785 return ListServer.ezmlm;
|
|
786 } else if (mList[0].indexOf("@yahoogr")>0 || mList[0].indexOf("@gruposyahoo")>0) {
|
|
787 return ListServer.yahoo;
|
|
788 } else if (mList[0].indexOf("@googlegroups")>0) {
|
|
789 return ListServer.google;
|
|
790 }
|
|
791 }
|
|
792
|
|
793 String[] listproc = mail.getHeader("X-Listprocessor-Version");
|
|
794 if (listproc!=null && listproc.length==1 && listproc[0]!=null) {
|
|
795 if (listproc[0].indexOf("ListProc")>=0) {
|
|
796 if (listproc[0].indexOf("CREN")>=0) {
|
|
797 return ListServer.listproc;
|
|
798 } else {
|
|
799 return ListServer.oldlistproc;
|
|
800 }
|
|
801 } else {
|
|
802 logger.error("unknown listproc version: "+listproc[0]+" in message "+mail.getMessageID());
|
|
803 return null;
|
|
804 }
|
|
805 }
|
|
806
|
|
807 String[] ecartis = mail.getHeader("X-ecartis-version");
|
|
808 if (ecartis!=null && ecartis.length==1 && ecartis[0]!=null) {
|
|
809 if (ecartis[0].indexOf("Ecartis")>=0) {
|
|
810 return ListServer.ecartis;
|
|
811 } else {
|
|
812 logger.error("unknown ecartis version: "+ecartis[0]+" in message "+mail.getMessageID());
|
|
813 return null;
|
|
814 }
|
|
815 }
|
|
816
|
|
817 String[] lyris = mail.getHeader("X-LISTMANAGER-Message-Id");
|
|
818 if (lyris!=null && lyris.length==1 && lyris[0]!=null) {
|
|
819 if (lyris[0].indexOf("LISTMANAGER")>=0) {
|
|
820 return ListServer.lyris;
|
|
821 } else {
|
|
822 logger.error("unexpected x-listmanager-message-id header: "+lyris[0]+" in message "+mail.getMessageID());
|
|
823 return null;
|
|
824 }
|
|
825 }
|
|
826
|
|
827 String[] xListServer = mail.getHeader("X-ListServer");
|
|
828 if (xListServer!=null && xListServer.length==1 && xListServer[0]!=null) {
|
|
829 if (xListServer[0].indexOf("CommuniGate")>=0) {
|
|
830 return ListServer.communigate;
|
|
831 } else {
|
|
832 logger.error("unknown x-listserver header: "+xListServer[0]+" in message "+mail.getMessageID());
|
|
833 return null;
|
|
834 }
|
|
835 }
|
|
836
|
|
837 // may not be reliable
|
|
838 String[] listSubscribe = mail.getHeader("List-Subscribe");
|
|
839 if (listSubscribe!=null && listSubscribe.length==1 && listSubscribe[0]!=null) {
|
|
840 if (listSubscribe[0].indexOf("+subscribe@")>0) {
|
|
841 return ListServer.mlmmj;
|
|
842 } else if (listSubscribe[0].indexOf("listserver@")>=0) {
|
|
843 return ListServer.listserver;
|
|
844 } else if (listSubscribe[0].indexOf("sympa@")>=0) {
|
|
845 return ListServer.sympa;
|
|
846 }
|
|
847 }
|
|
848
|
|
849 // not possible to detect:
|
|
850 // listserv
|
|
851 // majordomo / majordomo2
|
|
852 // smartlist
|
|
853
|
|
854 return null;
|
|
855 }
|
|
856 /*
|
|
857 static void redoGuessedParents() {}
|
|
858
|
|
859 public static void rethreadPosts(boolean inBatch) throws SQLException{
|
|
860 rethreadPosts(1, 0, inBatch);
|
|
861 }
|
|
862
|
|
863 public static void rethreadPosts(long startingPostId, boolean inBatch) throws SQLException{
|
|
864 rethreadPosts(startingPostId, 0, inBatch);
|
|
865 }
|
|
866
|
|
867 static void rethreadPosts(long startingPostId, long forumId, boolean inBatch) throws SQLException{
|
|
868 Logger batchLog = inBatch ? Batch.logger : logger;
|
|
869 if(startingPostId > 1)
|
|
870 batchLog.info("Starting rethread from post " + startingPostId);
|
|
871 if(forumId > 0)
|
|
872 batchLog.info("Rethreading for forum " + forumId);
|
|
873
|
|
874 //the WHERE condition is post_id > postId !
|
|
875 long postId = startingPostId - 1;
|
|
876 int processed_post_count = 0;
|
|
877 int modified_post_count = 0;
|
|
878 int null_parent_count = 0;
|
|
879 NodeImpl post = null;
|
|
880 boolean more = true;
|
|
881 try {
|
|
882 outer: while (more) {
|
|
883 Connection con = Db.db.getConnection();
|
|
884 try {
|
|
885 con.setAutoCommit(false);
|
|
886 PreparedStatement stmt = con.prepareStatement(
|
|
887 (forumId > 0) ?
|
|
888 "SELECT * FROM descendants(" + forumId + ") WHERE node_id > ? AND " +
|
|
889 "(guessed_parent='t' OR guessed_parent is null) AND " +
|
|
890 "msg_fmt='m'" +
|
|
891 "ORDER BY node_id LIMIT 1"
|
|
892 :
|
|
893 "SELECT * FROM node WHERE node_id > ? AND " +
|
|
894 "(guessed_parent='t' OR guessed_parent is null) AND " +
|
|
895 "msg_fmt='m'" +
|
|
896 "ORDER BY node_id LIMIT 1"
|
|
897 );
|
|
898 stmt.setLong(1, postId);
|
|
899 ResultSet rs = stmt.executeQuery();
|
|
900 more = false;
|
|
901 while (rs.next()) {
|
|
902 more = true;
|
|
903 post = NodeImpl.getNode(rs);
|
|
904 try {
|
|
905 postId = post.getId();
|
|
906 Mail mail = MailHome.newMail(post.getMessage().getRaw());
|
|
907 MailingListImpl mailingList = post.getAppImpl().getAssociatedMailingListImpl();
|
|
908 if (mailingList==null) continue; // forum no longer a mailing list
|
|
909 List<NodeImpl> descendants = new ArrayList<NodeImpl>();
|
|
910 for( NodeImpl n : post.getDescendantImpls() ) {
|
|
911 descendants.add(n);
|
|
912 }
|
|
913 NodeImpl parent = guessParent(mail, post.getWhenCreated(), mailingList, post.getSubject(), descendants.toArray(new NodeImpl[0]));
|
|
914
|
|
915 if (parent != null && parent.getId() != post.getParentId()) {
|
|
916 try {
|
|
917 batchLog.debug("setting parent of " + post + " to " + parent);
|
|
918 post.setGuessedParent(parent);
|
|
919 if(++ modified_post_count % 100 == 0)
|
|
920 batchLog.info("Modified " + modified_post_count + " posts");
|
|
921 } catch (ModelException.NodeLoop e) {
|
|
922 batchLog.error("",e);
|
|
923 }
|
|
924 } else if(parent == null && post.getParentId() != 0 && post.getParent().getKind()!=Node.Kind.APP){
|
|
925 batchLog.info("Null parent at " + post);
|
|
926 null_parent_count ++;
|
|
927 }
|
|
928 } catch(Exception x){
|
|
929 batchLog.error("Exception at post " + post, x);
|
|
930 break outer;
|
|
931 }
|
|
932
|
|
933 if(++ processed_post_count % 3000 == 0)
|
|
934 batchLog.info("Processed " + processed_post_count + " posts, current postId: "+postId);
|
|
935
|
|
936 if (inBatch) {
|
|
937 Batch.checkStopped();
|
|
938 }
|
|
939 }
|
|
940 stmt.close();
|
|
941 con.commit();
|
|
942 } finally {
|
|
943 con.close();
|
|
944 }
|
|
945 }
|
|
946 } finally {
|
|
947 batchLog.info("Exited at post " + post);
|
|
948 batchLog.info("Processed " + processed_post_count + " posts");
|
|
949 batchLog.info("Modified " + modified_post_count + " posts");
|
|
950 batchLog.info("Guessed null parent at " + null_parent_count + " posts");
|
|
951 }
|
|
952 }
|
|
953 */
|
|
954 private static void getRethreadIds(Connection con,long parentId,Collection<Long> ids)
|
|
955 throws SQLException
|
|
956 {
|
|
957 PreparedStatement stmt = con.prepareStatement(
|
|
958 "select node_id, guessed_parent, msg_fmt from node where parent_id = ?"
|
|
959 );
|
|
960 stmt.setLong(1,parentId);
|
|
961 ResultSet rs = stmt.executeQuery();
|
|
962 while( rs.next() ) {
|
|
963 long id = rs.getLong("node_id");
|
|
964 if( "m".equals(rs.getString("msg_fmt"))
|
|
965 && ( rs.getBoolean("guessed_parent") || rs.wasNull() )
|
|
966 )
|
|
967 ids.add(id);
|
|
968 getRethreadIds(con,id,ids);
|
|
969 }
|
|
970 rs.close();
|
|
971 stmt.close();
|
|
972 }
|
|
973
|
|
974 static void rethreadForum(NodeImpl forum, boolean inBatch) throws SQLException{
|
|
975 long forumId = forum.getId();
|
|
976 long rethreadStart = System.currentTimeMillis();
|
|
977 Logger batchLog = inBatch ? Batch.logger : logger;
|
|
978 batchLog.info("Rethreading for forum " + forumId);
|
|
979 SiteKey siteKey = forum.getSiteImpl().siteKey;
|
|
980 DbDatabase db = siteKey.getDb();
|
|
981
|
|
982 Collection<Long> ids = new ArrayList<Long>();
|
|
983 {
|
|
984 Connection con = db.getConnection();
|
|
985 long queryStart = System.currentTimeMillis();
|
|
986 getRethreadIds(con,forumId,ids);
|
|
987 batchLog.info("Query took " + (System.currentTimeMillis() - queryStart) + " ms");
|
|
988 con.close();
|
|
989 }
|
|
990
|
|
991 batchLog.info(ids.size() + " posts to process...");
|
|
992
|
|
993 //the WHERE condition is post_id > postId !
|
|
994 int processed_post_count = 0;
|
|
995 int modified_post_count = 0;
|
|
996 int null_parent_count = 0;
|
|
997
|
|
998 try {
|
|
999 while (ids.size() > 0) {
|
|
1000 Connection con = db.getConnection();
|
|
1001 try {
|
|
1002 con.setAutoCommit(false);
|
|
1003
|
|
1004 long id = ids.iterator().next();
|
|
1005 ids.remove(id);
|
|
1006 NodeImpl post = NodeImpl.getNode(siteKey,id);
|
|
1007 try {
|
|
1008 Mail mail = MailHome.newMail(post.getMessage().getRaw());
|
|
1009 MailingListImpl mailingList = post.getAppImpl().getAssociatedMailingListImpl();
|
|
1010 if (mailingList==null) continue; // forum no longer a mailing list
|
|
1011 List<NodeImpl> descendants = new ArrayList<NodeImpl>();
|
|
1012 for( NodeImpl n : post.getDescendantImpls() ) {
|
|
1013 descendants.add(n);
|
|
1014 }
|
|
1015 NodeImpl parent = guessParent(mail, post.getWhenCreated(), mailingList, post.getSubject(), descendants.toArray(new NodeImpl[0]));
|
|
1016
|
|
1017 if (parent != null && parent.getId() != post.getParentId()) {
|
|
1018 try {
|
|
1019 batchLog.info("setting parent of " + post + " to " + parent);
|
|
1020 post.setGuessedParent(parent);
|
|
1021
|
|
1022 if(++ modified_post_count % 1000 == 0)
|
|
1023 batchLog.info("Modified " + modified_post_count + " posts");
|
|
1024 } catch (ModelException.NodeLoop e) {
|
|
1025 batchLog.error("",e);
|
|
1026 }
|
|
1027 } else if(parent == null && post.getParentId() != 0 && post.getParent().getKind()!=Node.Kind.APP){
|
|
1028 batchLog.info("Null parent at " + post);
|
|
1029 null_parent_count ++;
|
|
1030 }
|
|
1031 } catch(Exception x){
|
|
1032 batchLog.error("Exception at " + post + " - message:\n"+post.getMessage().getRaw(), x);
|
|
1033 break;
|
|
1034 }
|
|
1035
|
|
1036 if(++ processed_post_count % 1000 == 0)
|
|
1037 batchLog.info("Processed " + processed_post_count + " posts, current postId: "+id);
|
|
1038
|
|
1039 if (inBatch) {
|
|
1040 Batch.checkStopped();
|
|
1041 }
|
|
1042 con.commit();
|
|
1043 } finally {
|
|
1044 con.close();
|
|
1045 }
|
|
1046 }
|
|
1047 } finally {
|
|
1048 batchLog.info("Rethread took " + (System.currentTimeMillis() - rethreadStart) + " ms");
|
|
1049 batchLog.info("Processed " + processed_post_count + " posts");
|
|
1050 batchLog.info("Modified " + modified_post_count + " posts");
|
|
1051 batchLog.info("Guessed null parent at " + null_parent_count + " posts");
|
|
1052 }
|
|
1053 }
|
|
1054
|
|
1055
|
|
1056 /**
|
|
1057 * Get or create message id from an email
|
|
1058 *
|
|
1059 * @param mail email message
|
|
1060 * @param msgFmt message format to use
|
|
1061 * @return message id, never null
|
|
1062 */
|
|
1063 private static String getMessageID(Mail mail, Message.Format msgFmt) {
|
|
1064 String[] messageIds = mail.getHeader("Message-Id"); // returns both Id and ID
|
|
1065 if (messageIds == null || messageIds.length == 0 || messageIds[messageIds.length - 1] == null) {
|
|
1066 return calcMessageID(mail, msgFmt);
|
|
1067 } else {
|
|
1068 return MailSubsystem.stripBrackets(messageIds[messageIds.length - 1]);
|
|
1069 }
|
|
1070 }
|
|
1071
|
|
1072 /**
|
|
1073 * Create a new message if for an email message
|
|
1074 *
|
|
1075 * @param mail mail message to process
|
|
1076 * @param msgFmt message format to use
|
|
1077 * @return a new message id, never null
|
|
1078 */
|
|
1079 private static String calcMessageID(Mail mail, Message.Format msgFmt) {
|
|
1080 StringBuilder msgId = new StringBuilder();
|
|
1081 msgId.append("MissingID.");
|
|
1082 String text = msgFmt.getText(mail.getRawInput(),null);
|
|
1083 msgId.append(Integer.toHexString(text.hashCode()));
|
|
1084 MailAddress from = mail.getFrom();
|
|
1085 if (from != null) msgId.append(Integer.toHexString(from.toString().hashCode()));
|
|
1086 MailAddress[] to = mail.getTo();
|
|
1087 if (to != null && to.length > 0) msgId.append(Integer.toHexString(to[0].toString().hashCode()));
|
|
1088 Date date = mail.getSentDate();
|
|
1089 if (date != null) msgId.append(Integer.toHexString(date.hashCode()));
|
|
1090 String subject = mail.getSubject();
|
|
1091 if (subject != null) msgId.append(Integer.toHexString(subject.hashCode()));
|
|
1092 msgId.append("@nabble.com");
|
|
1093 return msgId.toString();
|
|
1094 }
|
|
1095
|
|
1096
|
|
1097
|
|
1098
|
|
1099
|
|
1100 private static final Pop3Server fwdPop3Server = (Pop3Server)Init.get("fwdPop3Server");
|
|
1101
|
|
1102 private static class Lazy {
|
|
1103 static final String emailPrefix;
|
|
1104 static final String emailSuffix;
|
|
1105 static final Pattern pattern;
|
|
1106 static {
|
|
1107 String addrSpec = fwdPop3Server.getUsername();
|
|
1108 int ind = addrSpec.indexOf('@');
|
|
1109 emailPrefix = addrSpec.substring(0, ind) + "+";
|
|
1110 emailSuffix = addrSpec.substring(ind);
|
|
1111 pattern = Pattern.compile(
|
|
1112 Pattern.quote(emailPrefix) + "([^@]+)\\+([^@]+)\\+([^@]+)" + Pattern.quote(emailSuffix)
|
|
1113 , Pattern.CASE_INSENSITIVE
|
|
1114 );
|
|
1115 }
|
|
1116 }
|
|
1117
|
|
1118 private static void processFwds() {
|
|
1119 if( fwdPop3Server == null ) {
|
|
1120 logger.error("fwdPop3Server not defined");
|
|
1121 System.exit(-1);
|
|
1122 }
|
|
1123 MailIterator mails = fwdPop3Server.getMail();
|
|
1124 try {
|
|
1125 while( mails.hasNext() ) {
|
|
1126 Mail mail = mails.next();
|
|
1127 try {
|
|
1128 fwdMail(mail);
|
|
1129 } catch (Exception e) {
|
|
1130 logger.error("mail:\n"+mail.getRawInput(),e);
|
|
1131 }
|
|
1132 }
|
|
1133 } finally {
|
|
1134 mails.close();
|
|
1135 }
|
|
1136 }
|
|
1137
|
|
1138 private static void fwdMail(Mail mail) {
|
|
1139 String[] envTo = mail.getHeader("Envelope-To");
|
|
1140 if (envTo == null)
|
|
1141 envTo = mail.getHeader("X-Delivered-to"); // fastmail
|
|
1142 if (envTo == null)
|
|
1143 envTo = mail.getHeader("X-Original-To"); // postfix
|
|
1144 String originalTo = envTo[0];
|
|
1145 Matcher matcher = Lazy.pattern.matcher(originalTo);
|
|
1146 if( !matcher.matches() )
|
|
1147 throw new RuntimeException("invalid email: "+originalTo);
|
|
1148 String fwdFrom = emailDecode(matcher.group(1));
|
|
1149 String fwdTo = emailDecode(matcher.group(2));
|
|
1150 if( (fwdFrom+fwdTo).hashCode() != Integer.parseInt(matcher.group(3)) )
|
|
1151 throw new RuntimeException("invalid hash: "+originalTo);
|
|
1152 mail.setFrom(new MailAddress(fwdFrom));
|
|
1153 mail.setTo(new MailAddress(fwdTo));
|
|
1154 logger.info("Forwarding email to mailing list: " + fwdTo);
|
|
1155 MailHome.getDefaultSmtpServer().send(mail);
|
|
1156 }
|
|
1157
|
|
1158 private static String fwdEmail(String from,String to) {
|
|
1159 return Lazy.emailPrefix + emailEncode(from) + '+' + emailEncode(to) + '+' + (from+to).hashCode() + Lazy.emailSuffix;
|
|
1160 }
|
|
1161
|
|
1162 private static String emailEncode(String s) {
|
|
1163 return HtmlUtils.urlEncode(s).replace('%','~');
|
|
1164 }
|
|
1165
|
|
1166 private static String emailDecode(String s) {
|
|
1167 return HtmlUtils.urlDecode(s.replace('~','%'));
|
|
1168 }
|
|
1169
|
|
1170 }
|