wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Mail Merge with XPages


Being able to have individualized letters based on a template was one of the drivers to make Word processors popular. In the age of mass-communication of one. This tasks falls no longer to the printer, but your eMail processor. For a complete solution, check out Chris Toohey's excellent Mailer application, that is yours for USD 5.00 only.
I was wondering what it would take to build something similar, minus the address management, in XPages. I defined a few constraints:
  • I don't want to store the address list or the raw message
  • I need to be able to send images
  • I need to be able to fine tune the HTML
  • No sending of attachments required
  • Message needs to be individual using parameters (in the Body only), list of parameters will be flexible supplied together with the eMails as CSV data
So I created the MergeManager Java bean. Along the way I made a few experiences:
  • When bound to a bean the XPages RichText control wants a data type of com.ibm.xsp.http.MimeMultipart
  • The IBM Image upload doesn't work with a bean bound RichText control, but dragging and dropping works. I got rid of the button with a single line of code:
    config.removeButtons = 'IbmImage';
  • When adding the Dropdown for the variables, a simple change allowed me to show the View source button:
    {"name" : "mustache", "items" : ["mustache","Source"] });
  • I reused parts of Toni's eMail bean, which I stripped of the attachment function and added direct deposition into the mail.box. Works like a charm
  • There are lots of loose ends to consider: using the OpenNTF Domino API to gain easy access to theads to send the messages in the background would be top on the list. Allow template storage, different messages for HTML and Text (and EE), eMail preview some of the others. But that's a story for another time
The result is a reusable bean for custom mass eMails.In my implementation I tested it on Domino (and the Notes client) 9.,0.1FP2. The bean should run in most 8.5x ++ versions, but the UI (not part of this post, subject to a future one) used the new CKEditor 4.2, so it won't run on older versions.
package com.notessensei;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;

import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.MIMEEntity;
import lotus.domino.MIMEHeader;
import lotus.domino.NotesException;
import lotus.domino.Session;
import lotus.domino.Stream;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.ibm.xsp.http.MimeMultipart;

public class MergeManager implements Serializable {

	private static final long	                   serialVersionUID	= 1L;
	private String	                               subject;
	private String	                               from;
	private String	                               rawAddresses;
	private final Map<String, Map<String, String>>	rcpData	        = new TreeMap<String, Map<String, String>>();
	private MimeMultipart	                       message;
	private String	                               eMailField	    = "eMail";
	private final ArrayList<String>	               fields	        = new ArrayList<String>();
	private final MustacheFactory	               factory	        = new DefaultMustacheFactory();
	private final StringBuilder	                   statusMessages	= new StringBuilder();

	public MergeManager() {
		// Default Constructor to
		// be able to work as a bean
	}

	private void addOneDataLine(Map<String, Map<String, String>> allRCP, ArrayList<String> fields, String raw) {
		Map<String, String> oneLine = new HashMap<String, String>();
		String key = raw; // Backup pla
		try {
			String[] dataItems = raw.split(",");
			for (int i = 0; i < dataItems.length; i++) {
				oneLine.put(fields.get(i), dataItems[i]);
				if (fields.get(i).equalsIgnoreCase(this.eMailField)) {
					key = dataItems[i];
				}
			}
			allRCP.put(key, oneLine);

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	public void clear() {
		this.message = null;
		this.subject = null;
		this.rawAddresses = null;
		this.statusMessages.delete(0, this.statusMessages.length() - 1);
	}

	public String getCount() {
		return (this.rcpData.isEmpty()) ? "0" : String.valueOf(this.rcpData.size());
	}

	public String getEmailField() {
		return this.eMailField;
	}

	public ArrayList<String> getFields() {
		return this.fields;
	}

	private void getFields(ArrayList<String> list, String raw) {
		String[] fieldNames = raw.split(",");
		list.clear();
		for (int i = 0; i < fieldNames.length; i++) {
			list.add(fieldNames[i]);
		}
		return;
	}

	public String getFrom() {
		return this.from;
	}

	public MimeMultipart getMessage() {
		return this.message;
	}

	public String getRawAddresses() {
		return (this.rawAddresses == null) ? ("eMail,salutation,greeting\n"+
                                  "peter.parker@noreply.com,Hi spidy,Your arachno fan\n"+
                                  "st.wissel@sg.ibm.com,Dear NotesSensei,Keep on coding"
		        : this.rawAddresses;
	}

	public String getStatus() {
		return this.statusMessages.toString();
	}

	public String getSubject() {
		return this.subject;
	}

	private void populateMail(Session s, Document mail, String htmlBody,String textBody, String receipient) throws NotesException {
		s.setConvertMime(false);

		final MIMEEntity emailRoot = mail.createMIMEEntity("Body");

		// Primary Header
		String fromSender = this.getFrom();
		MIMEHeader emailHeader = emailRoot.createHeader("Return-Path");
		emailHeader.setHeaderVal(fromSender);
		emailHeader = emailRoot.createHeader("From");
		emailHeader.setHeaderVal(fromSender);
		emailHeader = emailRoot.createHeader("Sender");
		emailHeader.setHeaderVal(fromSender);

		emailHeader = emailRoot.createHeader("Recipients");
		emailHeader.setHeaderVal(receipient);

		emailHeader = emailRoot.createHeader("To");
		emailHeader.setHeaderVal(receipient);
		
		emailHeader = emailRoot.createHeader("Subject");
		emailHeader.setHeaderVal(this.getSubject());

		// Text and HTML Body
		MIMEEntity emailRootChild = emailRoot.createChildEntity();
		final String boundary = System.currentTimeMillis() + "-" + String.valueOf(this.hashCode());
		emailHeader = emailRootChild.createHeader("Content-Type");
		emailHeader.setHeaderVal("multipart/alternative; boundary=\"" + boundary + "\"");

		MIMEEntity emailChild = emailRootChild.createChildEntity();

		Stream stream = s.createStream();
		stream.writeText(textBody);
		emailChild.setContentFromText(stream, "text/plain; charset=\"UTF-8\"", MIMEEntity.ENC_NONE);
		stream.close();

		emailChild = emailRootChild.createChildEntity();
		stream = s.createStream();
		stream.writeText(htmlBody);
		emailChild.setContentFromText(stream, "text/html; charset=\"UTF-8\"", MIMEEntity.ENC_NONE);
		stream.close();
		stream.recycle();
		stream = null;
		s.setConvertMime(true);
	}

	public void process(Session s, Database database, Database mailbox) {
		System.out.println(this.message.getHTML());
		System.out.println(this.message.getContentAsText());
		Mustache mHTML = this.factory.compile(new StringReader(this.message.getHTML()), "eMailHTML");
		Mustache mText = this.factory.compile(new StringReader(this.message.getContentAsText()), "eMailPlain");
		for (Map<String, String> oneRecord : this.rcpData.values()) {
			this.sendOneMessage(s, database, mailbox, mHTML, mText, oneRecord);
		}
	}

	private void processRawAdresses(String raw) {
		ByteArrayInputStream in = new ByteArrayInputStream(raw.getBytes());
		boolean firstLine = true;
		Scanner scanner = new Scanner(in);

		while (scanner.hasNextLine()) {
			if (firstLine) {
				this.rcpData.clear();
				this.getFields(this.fields, scanner.nextLine());
				firstLine = false;
			} else {
				this.addOneDataLine(this.rcpData, this.fields, scanner.nextLine());
			}
		}
		this.statusMessages.append(String.format("%x Records loaded and ready\n", this.rcpData.size()));
		try {
			in.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void sendOneMessage(Session s, Database database, Database mailbox, Mustache mHTML, Mustache mText, Map<String, String> oneRecord) {
		
		// Text as HTML and as plain Text
		StringWriter htmlBody = new StringWriter();
		StringWriter textBody = new StringWriter();
		mHTML.execute(htmlBody, oneRecord);
		mText.execute(textBody, oneRecord);
		try {
			Document mail = database.createDocument();
			String rcpt = oneRecord.get(this.eMailField);
			mail.replaceItemValue("Form", "Memo");
			this.populateMail(s, mail, htmlBody.toString(), textBody.toString(), rcpt);
			mail.save();
			if (mailbox != null) {
				mail.copyToDatabase(mailbox);
			}
			mail.recycle();
			this.statusMessages.append("Mail send to ");
			this.statusMessages.append(rcpt);
			this.statusMessages.append("\n");
		} catch (Exception e) {
			this.statusMessages.append(e.getMessage());
			this.statusMessages.append("\n");
		}
	}

	public void setEmailField(String mailField) {
		this.eMailField = mailField;
	}

	public void setFrom(String from) {
		this.from = from;
	}

	public void setMessage(MimeMultipart message) {
		this.message = message;
	}

	public void setRawAddresses(String rawAddresses) {
		if (rawAddresses != null && !rawAddresses.equals(this.rawAddresses)) {
			this.processRawAdresses(rawAddresses);
			this.rawAddresses = rawAddresses;
		}
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}
}

As usual YMMV

Posted by on 13 December 2014 | Comments (0) | categories: XPages

Comments

  1. No comments yet, be the first to comment