Mercurial Hosting > nabble
diff src/nabble/model/PostByEmail.java @ 0:7ecd1a4ef557
add content
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 21 Mar 2019 19:15:52 -0600 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/nabble/model/PostByEmail.java Thu Mar 21 19:15:52 2019 -0600 @@ -0,0 +1,431 @@ +package nabble.model; + +import fschmidt.db.DbDatabase; +import fschmidt.util.mail.Mail; +import fschmidt.util.mail.MailAddress; +import fschmidt.util.mail.MailException; +import fschmidt.util.mail.MailHome; +import fschmidt.util.mail.MailIterator; +import fschmidt.util.mail.PlainTextContent; +import fschmidt.util.mail.Pop3Server; +import nabble.naml.compiler.Command; +import nabble.naml.compiler.CommandSpec; +import nabble.naml.compiler.IPrintWriter; +import nabble.naml.compiler.Interpreter; +import nabble.naml.compiler.Namespace; +import nabble.naml.compiler.ScopedInterpreter; +import nabble.naml.compiler.Template; +import nabble.naml.compiler.TemplatePrintWriter; +import nabble.naml.namespaces.BasicNamespace; +import nabble.naml.namespaces.TemplateException; +import nabble.view.lib.Permissions; +import nabble.view.web.template.NabbleNamespace; +import nabble.view.web.template.NodeNamespace; +import nabble.view.web.template.UserNamespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +@Namespace ( + name = "post_by_email", + global = true +) +public final class PostByEmail { + private static final Logger logger = LoggerFactory.getLogger(PostByEmail.class); + + // I would like to get rid of this. + static final MailMessageFormat msgFmt = new MailMessageFormat('s', "subscription"); + + private static final Pop3Server pop3Server = (Pop3Server)Init.get("subscriptionsPop3Server"); + + static { + if (Init.hasDaemons) { + runSubscriptions(); + } + } + + private static class Lazy { + static final String emailPrefix; + static final String emailSuffix; + static final Pattern pattern; + static { + String addrSpec = pop3Server.getUsername(); + int ind = addrSpec.indexOf('@'); + emailPrefix = addrSpec.substring(0, ind) + "+"; + emailSuffix = addrSpec.substring(ind); + pattern = Pattern.compile( + "\\+s(\\d+)n(\\d+)h(\\d+)" + Pattern.quote(emailSuffix), + Pattern.CASE_INSENSITIVE); + } + } + + private static void runSubscriptions() { + if (pop3Server == null) { + logger.error("Subscriptions: no pop3 specified for subscriptions"); + return; + } + Executors.scheduleWithFixedDelay(new Runnable() { + + public void run() { + try { + processSubscriptions(); + processBounces(); + } catch(MailException e) { + logger.error("mail processing",e); + } + } + + }, 10, 10, TimeUnit.SECONDS ); + logger.info("Subscriptions: pop3 reading thread started"); + } + + private static void processSubscriptions() { + MailIterator mails = pop3Server.getMail(); + try { + while (mails.hasNext()) { + Mail mail = mails.next(); + try { + new PostByEmail(mail).processMessage(); + } catch (Exception e) { + logger.error("mail:\n"+mail.getRawInput(),e); + } + } + } finally { + mails.close(); + } + } + + + // begin non-static part + + private final Mail mail; + private String email; + private String messageId; + private String address; + private NodeImpl repliedToNode; + private NodeImpl postedNode; + private UserImpl mailAuthor; + + private PostByEmail(Mail mail) { + this.mail = mail; + } + + private void processMessage() { + if( MailSubsystem.getReturnPath(mail).equals("") ) { + logger.info("ignoring bounce"); + return; + } + + email = mail.getFrom().getAddrSpec(); + + String[] messageIds = mail.getHeader("Message-Id"); // returns both Id and ID + messageId = messageIds!=null && messageIds.length==1 ? MailSubsystem.stripBrackets(messageIds[0]) : null; + + String[] a = mail.getHeader("Envelope-To"); + if (a == null) + a = mail.getHeader("X-Original-To"); // postfix + if (a == null) + a = mail.getHeader("X-Delivered-to"); // fastmail + if (a.length > 1) + a = new String[] { a[0] }; + for( String s : a[0].split(",") ) { + address = s.trim(); + Matcher matcher = Lazy.pattern.matcher(address); + if( matcher.find() ) { + long siteId = Long.valueOf(matcher.group(1)); + SiteImpl site = SiteKey.getInstance(siteId).site(); + if( site != null ) { + long nodeId = Long.valueOf(matcher.group(2)); + repliedToNode = site.getNodeImpl(nodeId); + if( repliedToNode != null ) { + if( repliedToNode.getAssociatedMailingList() != null) { + sendFailureMail( "Nabble", failureMessagePrefix() + "You can't post by email to a mailing list archive." ); + continue; + } + mailAuthor = site.getUserImplFromEmail(email); + if( mailAuthor != null && !generateHash(mailAuthor,repliedToNode).equals(matcher.group(3)) ) + mailAuthor = null; + callNaml(); + continue; + } + } + } +//System.out.println("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqq "+address); + sendFailureMail( "Nabble", failureMessagePrefix() + "No forum exists for this address." ); + } + } + + private void callNaml() { + Site site = repliedToNode.getSite(); + Template template = site.getTemplate( "post by email", + BasicNamespace.class, NabbleNamespace.class, PostByEmail.class + ); + template.run( TemplatePrintWriter.NULL, Collections.<String,Object>emptyMap(), + new BasicNamespace(template), new NabbleNamespace(site), this + ); + } + + private String failureMessagePrefix() { + return + "Delivery to the following recipient failed permanently:\n\n" + + " " + address + "\n\n" + ; + } + + private void sendFailureMail(String fromName,String failureMessage) { +/* why? + MailSubsystem.bounce(mail, + "Delivery to the following recipient failed permanently:\n\n " + + address + "\n\n" + + failureMessage + "\n" + ); +*/ + Mail bounce = MailHome.newMail(); + bounce.setFrom( new MailAddress(ModelHome.noReply,fromName) ); + bounce.setTo( new MailAddress(email) ); + bounce.setSubject( "Delivery Status Notification (Failure)" ); + bounce.setHeader( "X-Failed-Recipients", mail.getHeader("Envelope-To") ); + StringBuilder content = new StringBuilder(); + content + .append( failureMessage ).append( "\n\n" ) + .append( "----- Original message -----\n\n" ) + .append( mail.getRawInput() ) + ; + bounce.setContent(new PlainTextContent(content.toString())); + ModelHome.send(bounce); + logger.warn("bouncing subscription mail for "+email); + } + + private UserImpl mailAuthor() throws TemplateException { + if( mailAuthor==null ) + throw TemplateException.newInstance("subscription_processing_bad_user"); + return mailAuthor; + } + + private void saveToPost() throws TemplateException { + logger.info("Processing email from: " + address); + + Date date = mail.getSentDate(); + Date now = new Date(); + if (date == null || date.compareTo(now) > 0 || date.getTime() < 0) { + date = now; + } + + String subject = mail.getSubject(); + if (subject == null || subject.trim().length() == 0) + subject = "(no subject)"; + + String message = mail.getRawInput(); + message = message.replace("\000",""); // postgres can't handle 0 + if( !msgFmt.isOk(message) ) + throw TemplateException.newInstance("bad_mail"); + UserImpl mailAuthor = mailAuthor(); + + logger.info("Making a post from a message from " + address); + DbDatabase db = repliedToNode.siteKey.getDb(); + db.beginTransaction(); + try { + postedNode = NodeImpl.newChildNode(Node.Kind.POST, mailAuthor, subject, message, msgFmt, repliedToNode); + postedNode.setWhenCreated(date); + if( messageId != null ) + postedNode.setMessageID(messageId); + + postedNode.checkNewPostLimit(); + + postedNode.insert(true); + + db.commitTransaction(); + } catch(ModelException e) { + logger.error("Subscription processing failed: " + mail.getRawInput(), e); + } finally { + db.endTransaction(); + } + } + + // naml + + public static final CommandSpec thread_by_subject = new CommandSpec.Builder() + .parameters("prefixes") + .build() + ; + + @Command public void thread_by_subject(IPrintWriter out,Interpreter interp) { + if( repliedToNode.getKind() == Node.Kind.APP ) + return; + String subject = mail.getSubject(); + if (subject == null) + return; + String prefixes = interp.getArgString("prefixes"); + Pattern prefixRegex = MailingLists.prefixRegex(prefixes); + if( MailingLists.normalizeSubject(subject,prefixRegex).equals(MailingLists.normalizeSubject(repliedToNode.getSubject(),prefixRegex)) ) + return; + NodeImpl app = repliedToNode.getAppImpl(); + if( app != null ) + repliedToNode = app; + } + + @Command public void new_post_subject(IPrintWriter out,Interpreter interp) { + out.print(mail.getSubject()); + } + + public static final CommandSpec set_new_post_subject = new CommandSpec.Builder() + .dotParameter("subject") + .build() + ; + + @Command public void set_new_post_subject(IPrintWriter out,Interpreter interp) { + String newSubject = interp.getArgString("subject"); + mail.setSubject(newSubject); + } + + + public static final CommandSpec save_to_post = CommandSpec.NO_OUTPUT; + + @Command public void save_to_post(IPrintWriter out,Interpreter interp) throws TemplateException { + saveToPost(); + } + + public static final CommandSpec send_failure_mail = CommandSpec.NO_OUTPUT() + .dotParameter("text") + .optionalParameters("from") + .build() + ; + + @Command public void send_failure_mail(IPrintWriter out,Interpreter interp) { + String from = interp.getArgString("from"); + if( from == null ) + from = "Nabble"; + String msg = interp.getArgString("text"); + sendFailureMail(from,msg); + } + + @Command public void email_from(IPrintWriter out,Interpreter interp) throws TemplateException { + out.print( email ); + } + + @Command public void email_to(IPrintWriter out,Interpreter interp) throws TemplateException { + out.print( address ); + } + + public static final CommandSpec replied_to_node = CommandSpec.DO; + + @Command public void replied_to_node(IPrintWriter out,ScopedInterpreter<NodeNamespace> interp) { + out.print( interp.getArg( new NodeNamespace(repliedToNode), "do" ) ); + } + + public static final CommandSpec posted_node = CommandSpec.DO; + + @Command public void posted_node(IPrintWriter out,ScopedInterpreter<NodeNamespace> interp) { + out.print( interp.getArg( new NodeNamespace(postedNode), "do" ) ); + } + + public static final CommandSpec mail_author = CommandSpec.DO; + + @Command public void mail_author(IPrintWriter out,ScopedInterpreter<UserNamespace> interp) + throws TemplateException + { + out.print( interp.getArg( new UserNamespace(mailAuthor()), "do" ) ); + } + + // end non-static part + + + static String getMailAddress(User user, Node node) { + Site site = user.getSite(); + if( !node.getSite().equals(site) ) + throw new RuntimeException(); + return + Lazy.emailPrefix + + 's' + site.getId() + + 'n' + node.getId() + + 'h' + generateHash(user,node) + + Lazy.emailSuffix + ; + } + + + private static String generateHash(User user,Node node) { + int h = user.hashCode(); + h = 31*h + node.hashCode(); + h = 31*h + 23; + return Integer.toString(Math.abs(h)%100); + } + + + + + + + + private static final Pop3Server bouncesPop3Server = (Pop3Server)Init.get("subscriptionBouncesPop3Server"); + + private static class LazyBounces { + static final String emailPrefix; + static final String emailSuffix; + static final Pattern pattern; + static { + String addrSpec = bouncesPop3Server.getUsername(); + int ind = addrSpec.indexOf('@'); + emailPrefix = addrSpec.substring(0, ind) + "+"; + emailSuffix = addrSpec.substring(ind); + pattern = Pattern.compile( + "\\+s(\\d+)u(\\d+)" + Pattern.quote(emailSuffix) + , Pattern.CASE_INSENSITIVE + ); + } + } + + private static synchronized void processBounces() { + if( bouncesPop3Server == null ) { + logger.error("subscriptionBouncesPop3Server not defined"); + System.exit(-1); + } + MailIterator mails = bouncesPop3Server.getMail(); + try { + while( mails.hasNext() ) { + Mail mail = mails.next(); + try { + processBounce(mail); + } catch (Exception e) { + logger.error("mail:\n"+mail.getRawInput(),e); + } + } + } finally { + mails.close(); + } + } + + private static void processBounce(Mail mail) { + String[] envTo = mail.getHeader("Envelope-To"); + if (envTo == null) + envTo = mail.getHeader("X-Original-To"); // postfix + if (envTo == null) + envTo = mail.getHeader("X-Delivered-to"); // fastmail + String originalTo = envTo[0]; + Matcher matcher = LazyBounces.pattern.matcher(originalTo); + if( !matcher.find() ) + throw new RuntimeException("invalid email: "+originalTo); + long siteId = Long.parseLong( matcher.group(1) ); + long userId = Long.parseLong( matcher.group(2) ); + SiteImpl site = SiteKey.getInstance(siteId).site(); + UserImpl user = site.getUserImpl(userId); + user.bounced(); + logger.info(""+user+" has "+user.getBounces()+" bounces"); + } + + static String getBouncesAddress(User user) { + return + LazyBounces.emailPrefix + + 's' + user.getSite().getId() + + 'u' + user.getId() + + LazyBounces.emailSuffix + ; + } + +}