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
+		;
+	}
+
+}