How to generate and send emails using Spring and Velocity

Testing applications that communicate using email is more challenging than e.g. testing database access. Thanks to GMail and Spring, it can be done pretty easily.

Here is the scoop: I need to generate email notifying users about business events and after sending the email, store the content in the database. In real environment you will use corporate SMTP server and real email addresses of real people.
For development we can avoid bothering and spamming our customers by few simple tricks.

First step is to get new GMail account. Name it e.g. Company.Notifier or something easily distinguishable. In Spring, configure the sender bean:


           
                  smtp.gmail.com
           
           
                  
                          true
                          
                          true
                          25000
                
            
            
                   company.notifier
            
            
                   secret
            
            
            <!--   you don’t need to set the port number, 25 is default -->
     

The port can have value 25 (default) – but if you are using ISP provider such as Rogers, chances are the default port is blocked for outgoing connections – you can use port 587 instead.Second piece is component that actually uses the mailSender: notificationService.

    
           
           
           
       
           
               miro.adamy+alwaysCC@thinknostic.com
           
       

    .... deleted ...

   

Note the velocityEngine bean that is used to generate the body of the email from the template. The ‘alwaysCCList’ property is using nice little trick available with GMail: if you send email to YOURNAME+anything@gmail.com, the ‘+anything’ will be ignored but kept with email and it will arrive as if the address were just YOURNAME@gmail.com. You can use the postfix to find or autotag the emails.The code that sends email is actually pretty simple (the method of the notificationService)

public EmailContent sendNotificationEmail(EmailParameters params, EmailContent _content, boolean isDryRun) {

    final EmailContent c = mergeTemplate(params, _content);

    MimeMessagePreparator preparator = new MimeMessagePreparator() {
         public void prepare(MimeMessage mimeMessage) throws Exception {
            MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
            message.setTo(c.getEmailTo());
            message.setFrom("DO-NOT-REPLY@company.com"); // could be parameterized...

            message.setText(c.getEmailBody(), true);
            message.setSubject(c.getEmailSubject());

            if (alwaysCCList != null &amp;&amp; alwaysCCList.size() &gt; 0) {
                message.setCc(alwaysCCList.toArray(new String[0]));
                c.setEmailCcFromList(alwaysCCList);
            }
            ...
         }
      };

      if (isDryRun || emailsToFile)
      {
              // save to file
            ...
      }

      if (!isDryRun)
          this.mailSender.send(preparator);
      return c;
}

The class EmailContent is container for email address, subject, body, CC list.
It gets created as empty class with only recipient name and email address passed from business method as well as name of the Velocity template that is used to render email body. The method mergeTemplate loads the Velocity template and renders the actual email body, using the parameters (which is more or less) a hash map, cotaining e.g. URL’s or information that needs to be inserted to email. The generated content is stored back to EmailContent, which will be after successful delivery written to database for audit and archiving purposes.If you are JavaScript or Ruby programmer, you will immediately recognize the ‘functor’ pattern: preparator is set to instance of anonymous inner class and is used with configured preparator.The actual rendering of the content using Velocity can be done like this:


private EmailContent mergeTemplate( EmailParameters params, EmailContent content) {
    Map model = new HashMap();
    model.put("param", params);
    model.put("content", content);

    String text = "MISSING TEXT";
    String subject = "Notification email";
    String template = content.getTemplateId();
    try {

        // get subject line

        if (template_names.containsKey(template)) {
            subject = template_names.get(template);
            VelocityContext velocityContext = new VelocityContext(model);
             StringWriter writer = new StringWriter (  ) ;
             PrintWriter out = new PrintWriter ( writer ) ;

             Velocity.evaluate(velocityContext, out, "subject", new StringReader(subject));
             subject = writer.toString();

             model.put("subjectLine", subject);
            // now the body
             text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine,
                    "com/thinknostic/APP/service/email/"+template, model);

        }
        else {
            // TODO: log configuration error - template not found
        }

        content.setEmailBody(text);
        content.setEmailSubject(subject);
        return content;

    } catch (VelocityException e) {
        // back to untranslated
        // TODO: error report
        // subject = params.subject;
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    return null;
}

In the above, template id is actual file name with body of the HTML email and the hashmap template_names (configured in Spring XML) maps the id to another string, which is then used as subject line. Both body and subject can contain macros.
Note: if you get conflict on ${} syntax, see this entry.

Now, finally – how do we test this ? It is quite easy, thanks to JUnit support in Spring.


@ContextConfiguration(locations={"/com/thinknostic/APP/test/test-context.xml",
"/com/thinknostic/APP/test/service/NotificationTests-context.xml"})
public class NotificationTests extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    NotificationServiceImpl notificationService;
    public NotificationServiceImpl getNotificationService() {
        return notificationService;
    }
    public void setNotificationService(NotificationServiceImpl notificationService) {
        this.notificationService = notificationService;
    }

    @Autowired
    ApprovalDAO        approvalDao;
    public ApprovalDAO getApprovalDao() {
        return approvalDao;
    }
    public void setApprovalDao(ApprovalDAO approvalDao) {
        this.approvalDao = approvalDao;
    }

      Document doc;
      EmailContent cont;
      Approval app;
      ApprovalStep step1;
      ApprovalStep step2;

      @Before
      public void createMockApproval() {
          this.logger.info("Before");
          // controlls whether to use emails
          notificationService.setReallySend(true);

          doc = (Document)DocumentInfo.createMockDI("103", 1);

          // create the approval and insert it into
          app = Approval.createMockApproval(1L, doc);
          approvalDao.insert(app);

          step1 = ApprovalStep.createMockStep(app.getApprId(), 1);
          approvalDao.insert(step1);
          step2 = ApprovalStep.createMockStep(app.getApprId(), 2);
          approvalDao.insert(step2);

          cont = EmailContent.createMock(app.getApprId(), step1.getStep(), "miro adamy", "miro_adamy@rogers.com");

      }

      @After
      public void cleanup() {
          this.logger.info("After");
          // reset
          notificationService.setReallySend(false);
          doc = null;
          cont = null;
          app = null;
          step1 = null;
          step2 = null;
      }

      private void assertEverything()
      {
          assertTrue("The email service is empty", notificationService != null);
        assertTrue("The email parameteres not available", emailParameters != null);
        assertTrue("The document does not exist", doc != null);
      }

    @Test
    public void approval_all_cancelled_test() {
        doc.setFilename("approval_all_cancelled");

        notificationService.notifyApprovalCancelledAll(doc, cont);
    }

    @Test
    public void approval_cancelled_test() {
        assertEverything();

        doc.setFilename("approval_cancelled");
        EmailContent ui = EmailContent.createMock(app.getApprId(), step1.getStep(), "miro adamy", "Miro.Adamy@gmail.com");

        notificationService.notifyApprovalCancelled(doc, ui);
    }

    // and so on - many more tests
    ....
}

Assumed that your Spring context configuration files defined in annotations are OK, all you have to do is run the suite. The XML defines beans with names corresponding to the test properties and autowiring will take care of the rest.Also notice that @Before method (which runs before each test) actually DOES modify the database and inserts records with same primary key over and over. This works thanks to transaction magic of Spring, which rolls back the inserts at the end of the test.Last thing to mention is using of “mockXXX” static methods to generate the instance of object. IT not really a mock object we are creating here, but a well defined instance of business object with some properties parametrized. You can even define it as a bean in Spring context XML, but that is IMHO an overkill – keeping everything in test Java class makes things easier to understand.

This method does belong to tests, but it is very convenient just to keep it with domain object. I usually create the ‘createMock’ method right after I add a new domain object

Advertisements
Explore posts in the same categories: Java, springframework

One Comment on “How to generate and send emails using Spring and Velocity”

  1. Peter M. Says:

    I agree that rolling back transaction at the end of Spring tests greatly simplifies cleanup. Just don’t forget additional tests that will cover commit, if relevant for your application.
    Typical example: deferred database constraints. An application stubbornly passed all (rollbacking) unit tests, ignoring my attempts to reproduce a bug. Of course, there was a foreign key violation, patiently waiting for a commit to show up.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: