view 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 source

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

}