Friday, July 19, 2013

Use Spring JavaMailSender and Freemarker to send Newsletter from your JSF2 applications

Newsletters are a very powerful way to keep in touch with your web site users. They also are widely used as a mean of marketing. So how to generate and send a Newsletter in your JSF2 application ?

1) Use a template engine

As stated in Wikipedia, a template engine is "a software that is designed to process web templates and content information to produce output web documents".
So the idea is very simple, like when dealing with Facelets pages, we define a template page for our Newsletter, and then use the template engine to generate a new text based on merging this template and data we pass to it.
There are so many template engines in the open source market. Between them there is Freemarker, Velocity, StringTemplate, Thymeleaf and so many others. Personally I worked with Velocity and Freemarker. Both are very flexible and very powerful. 
If you want to use Velocity with your Newsletters you can find a little example for Spring integration here. In this article we will be using Freemarker.

2) Pick a template for Newsletter

First thing to do (just as when developing a web page) is to design our Newsletter. You can ask your designer to create a static pure HTML Newsletter template. For me, I just chose this free template. And here is a screenshot of it:

It's quite simple. It contains a list of head titles (under "In this issue"). It contains also a list of latest articles: every article will contain a title, a description and eventually an image (the image can be null). The newsletter will also contain a link to unsubscribe from our mailing list. 

3) Add Maven dependencies

You need to add Freemarker, JavaMail (required by Spring mail) and if you didn't already include it, Spring Context support. So in your POM file, make sure to include these dependencies:



  org.springframework
  spring-context-support
  ${org.springframework.version}



 javax.mail
 mail
 1.4.7

        

 org.freemarker
 freemarker
 2.3.14

4) Data model

Now we need to prepare our data model (if you didn't already) for the newsletter. As I said, we will display a list header titles (let's say this will present flash news), and a list of latest articles and an unsubscribe link.
This is our Article class:

package com.raissi.domain.newsletter;

import java.io.Serializable;

public class Article implements Serializable{
	private static final long serialVersionUID = 2999207145055407788L;

	private String title;
	private String image;
	private String description;
	
	public Article() {
		super();
	}
	public Article(String title, String image, String description) {
		super();
		this.title = title;
		this.image = image;
		this.description = description;
	}
	
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getImage() {
		return image;
	}
	public void setImage(String image) {
		this.image = image;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}		
}
For simplicity matter, I will use just a map of (title,url) pairs to display header titles. As for the unsubscribe link, it will point to unsbscribe-newsletter?token=encryptedUserEmail.

5) Implementation

5-a) Spring config

Spring provides a JavaMailSender utility that helps with handling mails, we will use it, 

	
	
	
	
		
			${mail.smtp.auth}
			${mail.smtp.port}
			${mail.host}
			true
		
	

Now add the Freemarker Configuration bean factory:




	
	
	

I think comments well explain each element in the above config.
Now let's create a service class that will be responsible for processing the template, generating the mail message and sending it via defined mailSender bean, in Spring add:

	
	

5-b) Service classes

And here is the MailService class:
package com.raissi.service.mail;

import java.util.Map;

import javax.mail.internet.MimeMessage;

import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import freemarker.template.Configuration;

public class MailService {

	private JavaMailSender javaMailSender;
	private Configuration freemarkerConfiguration;
	
	public void sendMail(final String from, final String to, final String subject, final Map model, final String template){
		MimeMessagePreparator preparator = new MimeMessagePreparator() {
	         public void prepare(MimeMessage mimeMessage) throws Exception {
	            MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
	            message.setFrom(from, "Raissi JSF2 sample");
       		    message.setTo(to);
       		    message.setSubject(subject);
       		    //template sample: "com/raissi/freemarker/confirm-register.ftl"
                String text = FreeMarkerTemplateUtils.processTemplateIntoString(freemarkerConfiguration.getTemplate(template,"UTF-8"), model);
	            message.setText(text, true);
	         }
	      };
		javaMailSender.send(preparator);
	}

	public void setJavaMailSender(JavaMailSender javaMailSender) {
		this.javaMailSender = javaMailSender;
	}

	public void setFreemarkerConfiguration(Configuration freemarkerConfiguration) {
		this.freemarkerConfiguration = freemarkerConfiguration;
	}	
}
The only tricky part of this class is the FreeMarkerTemplateUtils.processTemplateIntoString call. This method "Process the specified FreeMarker template with the given model and write the result to the given Writer." As for the model parameter, it's typically a Map that contains model names as keys and model objects as values.
This service class will be used by every class desiring to send an email with Freemarker as Template Engine in our application.
Now let's define a NewsLetterService class that will fetch data to be filled into the newsletter and then call mailService.sendMail:

package com.raissi.service.newsletter.impl;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Named;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.raissi.domain.User;
import com.raissi.domain.newsletter.Article;
import com.raissi.service.UserService;
import com.raissi.service.mail.MailService;
import com.raissi.service.newsletter.NewsLetterService;

@Service("newsLetterService")
@Transactional
public class NewsLetterServiceImpl implements NewsLetterService{
	private static final long serialVersionUID = -6291547874161783407L;
	
	@Inject	
	private @Named("mailService")MailService mailService;
	@Inject
	private UserService userService;
	
	public void sendNewsLetter(User user){
		//Generate the Unsubscribe link, it will contain the user's email encrypted
		try {
			/*
			 * Will generate a complete url to the specified pageName and containing the tokenToBeEncrypted 
			 * as encrypted param, ex: http://mysite.com/confirm-registration?token=userNameEncrypted 
			 */
			String unsubscribeUrl = userService.generateUserToken("unsbscribe-newsletter", user.getEmail());
			//Get the site base url from the above url, and use it for images urls
			//It will be in the form: http://mysite.com/resources/freemarker
			String baseUrl = unsubscribeUrl.substring(0,unsubscribeUrl.indexOf("/unsbscribe")+1)+"resources/freemarker";
			List
latestArticles = getLatestArticles(); Map headerTitles = getHottestNews(); String newsSourceUrl = "http://www.richarddawkins.net/"; String newsSourceName = "Richard Dawkins Foundation for Reason and Science"; Map model = new HashMap(); model.put("unsubscribeUrl", unsubscribeUrl); model.put("latestArticles", latestArticles); model.put("headerTitles", headerTitles); model.put("newsSourceUrl", newsSourceUrl); model.put("newsSourceName", newsSourceName); model.put("baseUrl", baseUrl); mailService.sendMail("raissi.java@gmail.com", user.getEmail(), "Our Newsletter", model, "com/raissi/freemarker/newsletter.ftl"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public Map getHottestNews(){ Map news = new HashMap(); //Just for testing purpose, we will generate a static list of news news.put("Richard Dawkins to headline unique Bristol event, Sat. 24th August","http://www.richarddawkins.net/news_articles/2013/7/19/richard-dawkins-to-headline-unique-bristol-event-sat-24th-august-2013"); news.put("Parliament 'must pardon codebreaker Turing'","http://www.richarddawkins.net/news_articles/2013/7/19/parliament-must-pardon-codebreaker-turing"); news.put("Curiosity team: Massive collision may have killed Red Planet","http://www.richarddawkins.net/news_articles/2013/7/19/curiosity-team-massive-collision-may-have-killed-red-planet"); news.put("Tyrannosaurus rex hunted for live prey","http://www.richarddawkins.net/news_articles/2013/7/18/tyrannosaurus-rex-hunted-for-live-prey"); return news; } public List
getLatestArticles(){ List
latestArticles = new ArrayList
(); //Just for testing purpose, we will generate a static list of Articles //Noam Chomsky String chomskyDesc = "Avram Noam Chomsky (/ˈnoʊm ˈtʃɒmski/; born December 7, 1928) is an American linguist," + " philosopher, cognitive scientist, logician, political critic, and activist. " + "He is an Institute Professor and Professor (Emeritus) in the Department of Linguistics & Philosophy at MIT, " + "where he has worked for over 50 years. " + "In addition to his work in linguistics, he has written on war, politics, and mass media, " + "and is the author of over 100 books.[13] Between 1980 and 1992, " + "Chomsky was cited within the field of Arts and Humanities more often than any other living scholar, " + "and eighth overall within the Arts and Humanities Citation Index during the same period." + " He has been described as a prominent cultural figure, and was voted the \"world's top public intellectual\" " + "in a 2005 poll.[18]"; String chomskyImg = "http://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Chomsky.jpg/200px-Chomsky.jpg"; Article noamChomsky = new Article("Noam Chomsky", chomskyImg, chomskyDesc); latestArticles.add(noamChomsky); //Richard Dawkins String dawkinsImg = "http://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Richard_Dawkins_Cooper_Union_Shankbone.jpg/250px-Richard_Dawkins_Cooper_Union_Shankbone.jpg"; String dawkinsDesc = "Clinton Richard Dawkins, FRS, FRSL (born 26 March 1941) is an English ethologist," + " evolutionary biologist and author. He is an emeritus fellow of New College, Oxford," + " and was the University of Oxford's Professor for Public Understanding of Science from 1995 until 2008."; Article richardDawkins = new Article("Richard Dawkins", dawkinsImg, dawkinsDesc); latestArticles.add(richardDawkins); //Stephen Hawking String hawkingDesc = "Stephen William Hawking CH, CBE, FRS, FRSA (Listeni/ˈstiːvɛn hoʊkɪŋ/; stee-ven hoh-king; born 8 January 1942) " + "is an English theoretical physicist, cosmologist, author and Director of Research at the Centre for Theoretical Cosmology" + " within the University of Cambridge. Among his significant scientific works have been a collaboration with " + "Roger Penrose on gravitational singularities theorems in the framework of general relativity, " + "and the theoretical prediction that black holes emit radiation, often called Hawking radiation." + " Hawking was the first to set forth a cosmology explained by a union of the general theory of " + "relativity and quantum mechanics. He is a vocal supporter of the many-worlds interpretation of quantum mechanics."; Article stephenHawking = new Article("Stephen Hawking", null, hawkingDesc); latestArticles.add(stephenHawking); //Paul Nizan String nizanImg = "http://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Nizanpaul.jpg/220px-Nizanpaul.jpg"; String nizanDesc = "Paul-Yves Nizan (French: [nizɑ̃]; 7 February 1905 – 23 May 1940) was a French philosopher and writer. " + "He was born in Tours, Indre-et-Loire and studied in Paris where he befriended fellow student Jean-Paul Sartre at the Lycée Henri IV." + " He became a member of the French Communist Party, and much of his writing reflects his political beliefs, " + "although he resigned from the party upon hearing of the Molotov-Ribbentrop Pact in 1939. " + "He died in the Battle of Dunkirk, fighting against the German army in World War II."; Article paulNizan = new Article("Paul Nizan", nizanImg, nizanDesc); latestArticles.add(paulNizan); return latestArticles; } }

The code of this class is very simple, there is only one thing that may be not clear. It's the call to userService.generateUserToken. In fact this method, is just a simple utility method to generate a url containing the specified token param encrypted and pointing to the passed page name param, here is its implementation:
public String generateUserToken(String pageName, String tokenToBeEncrypted) throws UnsupportedEncodingException{
		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
		String domain = "http://"+request.getServerName()+":"+request.getServerPort();
		String context = servletContext.getContextPath();
		//As in the getContextPath() docs, The path starts with a "/" character but does not end with a "/" character 
		context = domain+context;
		String encryptedToken = URLEncoder.encode(textEncryptor.encrypt(tokenToBeEncrypted),"UTF-8");
		return context+"/"+pageName+"?token="+encryptedToken;
}
Now everything is ready, but only the template page.

5-c) The Freemarker Template

To create the template, just copy the code of static HTML template that you chose (or your designer) and modify it by adding dynamic parts.
Let's start by the header titles. We said they are links to some news. In our Java data model, we passed them within a Map<String, String>: the title of the news is the key and the url is the value. The map is then put into the model param under the headerTitles key. To display this map in Freemarker, we use this syntax:

<#list headerTitles?keys as title>
    ${title}
</#list>

As for the latest articles list, we passed them as a List<Article> object under the key with value: latestArticles, and here is the Freemarker code to display that list:
<#list latestArticles as article>
${article.title}
<#if article.image??>																															</#if>
${article.description}
</#list>
Of course, here I omitted the CSS code and other design related HTML code.
As for the unsubscribe link, you should be able to guess its value:

Unsubscribe
As we passed the url to unsubscribe under the unsubscribeUrl within model param.
This is how my Newsletter seems as received in my Yahoo mail account (a part of, that my screen can display):

By now you should be able to send any kind of newsletter to your users.

6) Final (and very important) remarks

6-a) Inline styles

If you are using CSS styles defined in the head section of your template, then most of email clients will ignore it. See this link. So what will you do ? 
The answer is to use inline style, for example: 
instead of defining a style class that won't be recognized.
Now you be saying, but f**k how will I transform all those CSS classes into inline style? Well, the answer is with this awesome site.

6-b) Asynchronous execution

If you will use the above java MailService class as it's, and if you provide the user a button or a link to send him newsletter (or any other king of emails) when clicked, then the UI will be blocked until email is sent. And since this may take some long time (depending on your Email server and other Internet params), the process of sending emails should run asynchronously. You may think about using a Java Thread, which is completely legitimate.
The good news, is that Spring (with its great magic) offers the possibility of running methods asynchronously by simply adding an annotation: @Async. So in your MailService class annotate your sendMail with Async.
Note you must add the following directive (XML element) to your application context file, so that Spring will recognize the Async annotation:


    
    


And by now everything should be just perfect.
It will be great to see your comments

No comments:

Post a Comment