0
|
1 package nabble.model;
|
|
2
|
|
3 import fschmidt.db.DbDatabase;
|
|
4 import fschmidt.util.mail.Mail;
|
|
5 import fschmidt.util.mail.MailAddress;
|
|
6 import fschmidt.util.mail.MailException;
|
|
7 import fschmidt.util.mail.MailHome;
|
|
8 import fschmidt.util.mail.MailIterator;
|
|
9 import fschmidt.util.mail.PlainTextContent;
|
|
10 import fschmidt.util.mail.Pop3Server;
|
|
11 import nabble.naml.compiler.Command;
|
|
12 import nabble.naml.compiler.CommandSpec;
|
|
13 import nabble.naml.compiler.IPrintWriter;
|
|
14 import nabble.naml.compiler.Interpreter;
|
|
15 import nabble.naml.compiler.Namespace;
|
|
16 import nabble.naml.compiler.ScopedInterpreter;
|
|
17 import nabble.naml.compiler.Template;
|
|
18 import nabble.naml.compiler.TemplatePrintWriter;
|
|
19 import nabble.naml.namespaces.BasicNamespace;
|
|
20 import nabble.naml.namespaces.TemplateException;
|
|
21 import nabble.view.lib.Permissions;
|
|
22 import nabble.view.web.template.NabbleNamespace;
|
|
23 import nabble.view.web.template.NodeNamespace;
|
|
24 import nabble.view.web.template.UserNamespace;
|
|
25 import org.slf4j.Logger;
|
|
26 import org.slf4j.LoggerFactory;
|
|
27
|
|
28 import java.util.Collections;
|
|
29 import java.util.Date;
|
|
30 import java.util.concurrent.TimeUnit;
|
|
31 import java.util.regex.Matcher;
|
|
32 import java.util.regex.Pattern;
|
|
33
|
|
34
|
|
35 @Namespace (
|
|
36 name = "post_by_email",
|
|
37 global = true
|
|
38 )
|
|
39 public final class PostByEmail {
|
|
40 private static final Logger logger = LoggerFactory.getLogger(PostByEmail.class);
|
|
41
|
|
42 // I would like to get rid of this.
|
|
43 static final MailMessageFormat msgFmt = new MailMessageFormat('s', "subscription");
|
|
44
|
|
45 private static final Pop3Server pop3Server = (Pop3Server)Init.get("subscriptionsPop3Server");
|
|
46
|
|
47 static {
|
|
48 if (Init.hasDaemons) {
|
|
49 runSubscriptions();
|
|
50 }
|
|
51 }
|
|
52
|
|
53 private static class Lazy {
|
|
54 static final String emailPrefix;
|
|
55 static final String emailSuffix;
|
|
56 static final Pattern pattern;
|
|
57 static {
|
|
58 String addrSpec = pop3Server.getUsername();
|
|
59 int ind = addrSpec.indexOf('@');
|
|
60 emailPrefix = addrSpec.substring(0, ind) + "+";
|
|
61 emailSuffix = addrSpec.substring(ind);
|
|
62 pattern = Pattern.compile(
|
|
63 "\\+s(\\d+)n(\\d+)h(\\d+)" + Pattern.quote(emailSuffix),
|
|
64 Pattern.CASE_INSENSITIVE);
|
|
65 }
|
|
66 }
|
|
67
|
|
68 private static void runSubscriptions() {
|
|
69 if (pop3Server == null) {
|
|
70 logger.error("Subscriptions: no pop3 specified for subscriptions");
|
|
71 return;
|
|
72 }
|
|
73 Executors.scheduleWithFixedDelay(new Runnable() {
|
|
74
|
|
75 public void run() {
|
|
76 try {
|
|
77 processSubscriptions();
|
|
78 processBounces();
|
|
79 } catch(MailException e) {
|
|
80 logger.error("mail processing",e);
|
|
81 }
|
|
82 }
|
|
83
|
|
84 }, 10, 10, TimeUnit.SECONDS );
|
|
85 logger.info("Subscriptions: pop3 reading thread started");
|
|
86 }
|
|
87
|
|
88 private static void processSubscriptions() {
|
|
89 MailIterator mails = pop3Server.getMail();
|
|
90 try {
|
|
91 while (mails.hasNext()) {
|
|
92 Mail mail = mails.next();
|
|
93 try {
|
|
94 new PostByEmail(mail).processMessage();
|
|
95 } catch (Exception e) {
|
|
96 logger.error("mail:\n"+mail.getRawInput(),e);
|
|
97 }
|
|
98 }
|
|
99 } finally {
|
|
100 mails.close();
|
|
101 }
|
|
102 }
|
|
103
|
|
104
|
|
105 // begin non-static part
|
|
106
|
|
107 private final Mail mail;
|
|
108 private String email;
|
|
109 private String messageId;
|
|
110 private String address;
|
|
111 private NodeImpl repliedToNode;
|
|
112 private NodeImpl postedNode;
|
|
113 private UserImpl mailAuthor;
|
|
114
|
|
115 private PostByEmail(Mail mail) {
|
|
116 this.mail = mail;
|
|
117 }
|
|
118
|
|
119 private void processMessage() {
|
|
120 if( MailSubsystem.getReturnPath(mail).equals("") ) {
|
|
121 logger.info("ignoring bounce");
|
|
122 return;
|
|
123 }
|
|
124
|
|
125 email = mail.getFrom().getAddrSpec();
|
|
126
|
|
127 String[] messageIds = mail.getHeader("Message-Id"); // returns both Id and ID
|
|
128 messageId = messageIds!=null && messageIds.length==1 ? MailSubsystem.stripBrackets(messageIds[0]) : null;
|
|
129
|
|
130 String[] a = mail.getHeader("Envelope-To");
|
|
131 if (a == null)
|
|
132 a = mail.getHeader("X-Original-To"); // postfix
|
|
133 if (a == null)
|
|
134 a = mail.getHeader("X-Delivered-to"); // fastmail
|
|
135 if (a.length > 1)
|
|
136 a = new String[] { a[0] };
|
|
137 for( String s : a[0].split(",") ) {
|
|
138 address = s.trim();
|
|
139 Matcher matcher = Lazy.pattern.matcher(address);
|
|
140 if( matcher.find() ) {
|
|
141 long siteId = Long.valueOf(matcher.group(1));
|
|
142 SiteImpl site = SiteKey.getInstance(siteId).site();
|
|
143 if( site != null ) {
|
|
144 long nodeId = Long.valueOf(matcher.group(2));
|
|
145 repliedToNode = site.getNodeImpl(nodeId);
|
|
146 if( repliedToNode != null ) {
|
|
147 if( repliedToNode.getAssociatedMailingList() != null) {
|
|
148 sendFailureMail( "Nabble", failureMessagePrefix() + "You can't post by email to a mailing list archive." );
|
|
149 continue;
|
|
150 }
|
|
151 mailAuthor = site.getUserImplFromEmail(email);
|
|
152 if( mailAuthor != null && !generateHash(mailAuthor,repliedToNode).equals(matcher.group(3)) )
|
|
153 mailAuthor = null;
|
|
154 callNaml();
|
|
155 continue;
|
|
156 }
|
|
157 }
|
|
158 }
|
|
159 //System.out.println("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqq "+address);
|
|
160 sendFailureMail( "Nabble", failureMessagePrefix() + "No forum exists for this address." );
|
|
161 }
|
|
162 }
|
|
163
|
|
164 private void callNaml() {
|
|
165 Site site = repliedToNode.getSite();
|
|
166 Template template = site.getTemplate( "post by email",
|
|
167 BasicNamespace.class, NabbleNamespace.class, PostByEmail.class
|
|
168 );
|
|
169 template.run( TemplatePrintWriter.NULL, Collections.<String,Object>emptyMap(),
|
|
170 new BasicNamespace(template), new NabbleNamespace(site), this
|
|
171 );
|
|
172 }
|
|
173
|
|
174 private String failureMessagePrefix() {
|
|
175 return
|
|
176 "Delivery to the following recipient failed permanently:\n\n"
|
|
177 + " " + address + "\n\n"
|
|
178 ;
|
|
179 }
|
|
180
|
|
181 private void sendFailureMail(String fromName,String failureMessage) {
|
|
182 /* why?
|
|
183 MailSubsystem.bounce(mail,
|
|
184 "Delivery to the following recipient failed permanently:\n\n "
|
|
185 + address + "\n\n"
|
|
186 + failureMessage + "\n"
|
|
187 );
|
|
188 */
|
|
189 Mail bounce = MailHome.newMail();
|
|
190 bounce.setFrom( new MailAddress(ModelHome.noReply,fromName) );
|
|
191 bounce.setTo( new MailAddress(email) );
|
|
192 bounce.setSubject( "Delivery Status Notification (Failure)" );
|
|
193 bounce.setHeader( "X-Failed-Recipients", mail.getHeader("Envelope-To") );
|
|
194 StringBuilder content = new StringBuilder();
|
|
195 content
|
|
196 .append( failureMessage ).append( "\n\n" )
|
|
197 .append( "----- Original message -----\n\n" )
|
|
198 .append( mail.getRawInput() )
|
|
199 ;
|
|
200 bounce.setContent(new PlainTextContent(content.toString()));
|
|
201 ModelHome.send(bounce);
|
|
202 logger.warn("bouncing subscription mail for "+email);
|
|
203 }
|
|
204
|
|
205 private UserImpl mailAuthor() throws TemplateException {
|
|
206 if( mailAuthor==null )
|
|
207 throw TemplateException.newInstance("subscription_processing_bad_user");
|
|
208 return mailAuthor;
|
|
209 }
|
|
210
|
|
211 private void saveToPost() throws TemplateException {
|
|
212 logger.info("Processing email from: " + address);
|
|
213
|
|
214 Date date = mail.getSentDate();
|
|
215 Date now = new Date();
|
|
216 if (date == null || date.compareTo(now) > 0 || date.getTime() < 0) {
|
|
217 date = now;
|
|
218 }
|
|
219
|
|
220 String subject = mail.getSubject();
|
|
221 if (subject == null || subject.trim().length() == 0)
|
|
222 subject = "(no subject)";
|
|
223
|
|
224 String message = mail.getRawInput();
|
|
225 message = message.replace("\000",""); // postgres can't handle 0
|
|
226 if( !msgFmt.isOk(message) )
|
|
227 throw TemplateException.newInstance("bad_mail");
|
|
228 UserImpl mailAuthor = mailAuthor();
|
|
229
|
|
230 logger.info("Making a post from a message from " + address);
|
|
231 DbDatabase db = repliedToNode.siteKey.getDb();
|
|
232 db.beginTransaction();
|
|
233 try {
|
|
234 postedNode = NodeImpl.newChildNode(Node.Kind.POST, mailAuthor, subject, message, msgFmt, repliedToNode);
|
|
235 postedNode.setWhenCreated(date);
|
|
236 if( messageId != null )
|
|
237 postedNode.setMessageID(messageId);
|
|
238
|
|
239 postedNode.checkNewPostLimit();
|
|
240
|
|
241 postedNode.insert(true);
|
|
242
|
|
243 db.commitTransaction();
|
|
244 } catch(ModelException e) {
|
|
245 logger.error("Subscription processing failed: " + mail.getRawInput(), e);
|
|
246 } finally {
|
|
247 db.endTransaction();
|
|
248 }
|
|
249 }
|
|
250
|
|
251 // naml
|
|
252
|
|
253 public static final CommandSpec thread_by_subject = new CommandSpec.Builder()
|
|
254 .parameters("prefixes")
|
|
255 .build()
|
|
256 ;
|
|
257
|
|
258 @Command public void thread_by_subject(IPrintWriter out,Interpreter interp) {
|
|
259 if( repliedToNode.getKind() == Node.Kind.APP )
|
|
260 return;
|
|
261 String subject = mail.getSubject();
|
|
262 if (subject == null)
|
|
263 return;
|
|
264 String prefixes = interp.getArgString("prefixes");
|
|
265 Pattern prefixRegex = MailingLists.prefixRegex(prefixes);
|
|
266 if( MailingLists.normalizeSubject(subject,prefixRegex).equals(MailingLists.normalizeSubject(repliedToNode.getSubject(),prefixRegex)) )
|
|
267 return;
|
|
268 NodeImpl app = repliedToNode.getAppImpl();
|
|
269 if( app != null )
|
|
270 repliedToNode = app;
|
|
271 }
|
|
272
|
|
273 @Command public void new_post_subject(IPrintWriter out,Interpreter interp) {
|
|
274 out.print(mail.getSubject());
|
|
275 }
|
|
276
|
|
277 public static final CommandSpec set_new_post_subject = new CommandSpec.Builder()
|
|
278 .dotParameter("subject")
|
|
279 .build()
|
|
280 ;
|
|
281
|
|
282 @Command public void set_new_post_subject(IPrintWriter out,Interpreter interp) {
|
|
283 String newSubject = interp.getArgString("subject");
|
|
284 mail.setSubject(newSubject);
|
|
285 }
|
|
286
|
|
287
|
|
288 public static final CommandSpec save_to_post = CommandSpec.NO_OUTPUT;
|
|
289
|
|
290 @Command public void save_to_post(IPrintWriter out,Interpreter interp) throws TemplateException {
|
|
291 saveToPost();
|
|
292 }
|
|
293
|
|
294 public static final CommandSpec send_failure_mail = CommandSpec.NO_OUTPUT()
|
|
295 .dotParameter("text")
|
|
296 .optionalParameters("from")
|
|
297 .build()
|
|
298 ;
|
|
299
|
|
300 @Command public void send_failure_mail(IPrintWriter out,Interpreter interp) {
|
|
301 String from = interp.getArgString("from");
|
|
302 if( from == null )
|
|
303 from = "Nabble";
|
|
304 String msg = interp.getArgString("text");
|
|
305 sendFailureMail(from,msg);
|
|
306 }
|
|
307
|
|
308 @Command public void email_from(IPrintWriter out,Interpreter interp) throws TemplateException {
|
|
309 out.print( email );
|
|
310 }
|
|
311
|
|
312 @Command public void email_to(IPrintWriter out,Interpreter interp) throws TemplateException {
|
|
313 out.print( address );
|
|
314 }
|
|
315
|
|
316 public static final CommandSpec replied_to_node = CommandSpec.DO;
|
|
317
|
|
318 @Command public void replied_to_node(IPrintWriter out,ScopedInterpreter<NodeNamespace> interp) {
|
|
319 out.print( interp.getArg( new NodeNamespace(repliedToNode), "do" ) );
|
|
320 }
|
|
321
|
|
322 public static final CommandSpec posted_node = CommandSpec.DO;
|
|
323
|
|
324 @Command public void posted_node(IPrintWriter out,ScopedInterpreter<NodeNamespace> interp) {
|
|
325 out.print( interp.getArg( new NodeNamespace(postedNode), "do" ) );
|
|
326 }
|
|
327
|
|
328 public static final CommandSpec mail_author = CommandSpec.DO;
|
|
329
|
|
330 @Command public void mail_author(IPrintWriter out,ScopedInterpreter<UserNamespace> interp)
|
|
331 throws TemplateException
|
|
332 {
|
|
333 out.print( interp.getArg( new UserNamespace(mailAuthor()), "do" ) );
|
|
334 }
|
|
335
|
|
336 // end non-static part
|
|
337
|
|
338
|
|
339 static String getMailAddress(User user, Node node) {
|
|
340 Site site = user.getSite();
|
|
341 if( !node.getSite().equals(site) )
|
|
342 throw new RuntimeException();
|
|
343 return
|
|
344 Lazy.emailPrefix
|
|
345 + 's' + site.getId()
|
|
346 + 'n' + node.getId()
|
|
347 + 'h' + generateHash(user,node)
|
|
348 + Lazy.emailSuffix
|
|
349 ;
|
|
350 }
|
|
351
|
|
352
|
|
353 private static String generateHash(User user,Node node) {
|
|
354 int h = user.hashCode();
|
|
355 h = 31*h + node.hashCode();
|
|
356 h = 31*h + 23;
|
|
357 return Integer.toString(Math.abs(h)%100);
|
|
358 }
|
|
359
|
|
360
|
|
361
|
|
362
|
|
363
|
|
364
|
|
365
|
|
366 private static final Pop3Server bouncesPop3Server = (Pop3Server)Init.get("subscriptionBouncesPop3Server");
|
|
367
|
|
368 private static class LazyBounces {
|
|
369 static final String emailPrefix;
|
|
370 static final String emailSuffix;
|
|
371 static final Pattern pattern;
|
|
372 static {
|
|
373 String addrSpec = bouncesPop3Server.getUsername();
|
|
374 int ind = addrSpec.indexOf('@');
|
|
375 emailPrefix = addrSpec.substring(0, ind) + "+";
|
|
376 emailSuffix = addrSpec.substring(ind);
|
|
377 pattern = Pattern.compile(
|
|
378 "\\+s(\\d+)u(\\d+)" + Pattern.quote(emailSuffix)
|
|
379 , Pattern.CASE_INSENSITIVE
|
|
380 );
|
|
381 }
|
|
382 }
|
|
383
|
|
384 private static synchronized void processBounces() {
|
|
385 if( bouncesPop3Server == null ) {
|
|
386 logger.error("subscriptionBouncesPop3Server not defined");
|
|
387 System.exit(-1);
|
|
388 }
|
|
389 MailIterator mails = bouncesPop3Server.getMail();
|
|
390 try {
|
|
391 while( mails.hasNext() ) {
|
|
392 Mail mail = mails.next();
|
|
393 try {
|
|
394 processBounce(mail);
|
|
395 } catch (Exception e) {
|
|
396 logger.error("mail:\n"+mail.getRawInput(),e);
|
|
397 }
|
|
398 }
|
|
399 } finally {
|
|
400 mails.close();
|
|
401 }
|
|
402 }
|
|
403
|
|
404 private static void processBounce(Mail mail) {
|
|
405 String[] envTo = mail.getHeader("Envelope-To");
|
|
406 if (envTo == null)
|
|
407 envTo = mail.getHeader("X-Original-To"); // postfix
|
|
408 if (envTo == null)
|
|
409 envTo = mail.getHeader("X-Delivered-to"); // fastmail
|
|
410 String originalTo = envTo[0];
|
|
411 Matcher matcher = LazyBounces.pattern.matcher(originalTo);
|
|
412 if( !matcher.find() )
|
|
413 throw new RuntimeException("invalid email: "+originalTo);
|
|
414 long siteId = Long.parseLong( matcher.group(1) );
|
|
415 long userId = Long.parseLong( matcher.group(2) );
|
|
416 SiteImpl site = SiteKey.getInstance(siteId).site();
|
|
417 UserImpl user = site.getUserImpl(userId);
|
|
418 user.bounced();
|
|
419 logger.info(""+user+" has "+user.getBounces()+" bounces");
|
|
420 }
|
|
421
|
|
422 static String getBouncesAddress(User user) {
|
|
423 return
|
|
424 LazyBounces.emailPrefix
|
|
425 + 's' + user.getSite().getId()
|
|
426 + 'u' + user.getId()
|
|
427 + LazyBounces.emailSuffix
|
|
428 ;
|
|
429 }
|
|
430
|
|
431 }
|