AzoftCase StudiesImplementing Notifications on Undelivered Email Using javax.mail and Regular Expressions

Implementing Notifications on Undelivered Email Using javax.mail and Regular Expressions

By Eugene Ustimenko on January 30, 2014

implementing notifications using javamail Implementing Notifications on Undelivered Email Using javax.mail and Regular Expressions

While working on a recent project — a business automation system for an insurance company with one function that allowed for sending important, confidential e-mails to a select number of users simultaneously — our Azoft development team encountered a problem: The mail server with which our app worked couldn’t verify if an email recipient’s account was active. Since it is crucial for the company to know whether certain individuals have received an email or not, we enhanced the system to be able to process undelivered email and generate automatic sender notifications that include all necessary information (incorrect email address, specific error returned, etc.).

Today we'll share our undelivered email solution written in Java, v1.6. and give a brief overview of some alternative approaches.

The libraries used:

  • javax.mail — for mail processing
  • guava — for processing strings and collections

The challenge

Let's assume we are developing a considerably large multi-user business system where one function is to automatically inform users about certain operations, such as in our case here with undelivered email notifications; the number of daily sent emails is 1000; and all messages are sent from a single mailbox.

First, let's have a closer look at sending email. Each operation falls into two stages:

  • Filling in addressee data
  • Creating the email’s content and sending it

The mail server with which our app works can't check if the addressee really exists and simply sends an email to each address listed in the database. Usually, if an address doesn't exist,  in approximately a minute the sender receives an email notice that ‘delivery to the recipient has failed permanently.’

undelivered email Implementing Notifications on Undelivered Email Using javax.mail and Regular Expressions

The goals

  • Implement notification of undelivered email
  • Format notification as an email including all information in the error message

System criteria for solving the task

  • Sends messages only in a particular format
  • Mail server used only for sending messages
  • Mail server can process inaccurate email addresses, e.g., it returns an email when delivery to a recipient fails

Solution

Stage 1. Writing a module for connecting to the mail server

Before we move on to solving the task, we first need to write a module for connecting to the mail server. To do this we'll need the recipient’s email server configuration and their configuration for sending email. In the example below I've use the Gmail configuration. All email is sent via SSL protocol.

mail.username = your_mail_username
mail.password = your_mail_password
mail.protocol = imap
mail.host = imap.googlemail.com
mail.port = 993

mail.smtp.host = smtp.gmail.com
mail.smtp.port = 465
mail.smtp.auth = true
mail.smtp.socketFactory.port = 465
mail.smtp.socketFactory.class = javax.net.ssl.SSLSocketFactory
mail.smtp.username = your_smtp_username
mail.smtp.password = your_smtp_password

I'd recommend putting all configuration data in a separate file, so you don't need to get into the code to change it.

As you will notice, a username and password for sending and receiving email are defined separately. It's a moot point if you use Gmail because they are identical for both sending and receiving, but very often companies will use different authentication credentials for each of the processes.

Stage 2. Creating the MailService class 

Now we create the MailService class using three main methods to full fill the following functions:

  • Connecting to the mail server
  • Sending mail
  • Receiving mail
public class MailService {

    private static final Logger LOGGER = Logger.getLogger(MailService.class);

    private static final String MAIL_SMTP_AUTH = "mail.smtp.auth";
    private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable";
    private static final String MAIL_SMTP_HOST = "mail.smtp.host";
    private static final String MAIL_SMTP_PORT = "mail.smtp.port";
    private static final String MAIL_SMTP_SSL_SOCKETFACTORY = "mail.smtp.ssl.socketFactory";
private static final String MAIL_SERVER_PROTOCOL = “mail.server.protocol”;
private static final String MAIL_SERVER_PORT = “mail.server.port”;
private static final String MAIL_USERNAME = “mail.username”;
private static final String MAIL_PASSWORD = “mail.password”;
private static final String MAIL_SERVER_HOST = “mail.server.host”;

    private final String username;
    private final String password;
private final String serverHost;
private final String serverProtocol;
private final String port;
    private Session session;
private Folder mailInbox;

public MailService(){
Properties config = new Properties();
try{
	config.load(“file/path”); //path to file with mail configs created below
}catch(IOException e){
	LOGGER.error(e.getMessage(), e);
}
username = config.getProperty(MAIL_USERNAME);
password = config.getProperty(MAIL_PASSWORD);
serverHost = config.getProperty(MAIL_SERVER_HOST);
port = config.getProperty(MAIL_SERVER_PORT);
serverProtocol = config.getProperty(MAIL_SERVER_PROTOCOL);
}

//Sending message. The message could be created in different ways.
    public void sendEmail(Message message) throws Exception {
        if (null == session) {
            session = createUserMailSession();
        }
        Transport.send(message);        
    }

    private Session createUserMailSession() {
        return Session.getInstance(defaultMailConfig(),
                new javax.mail.Authenticator() {
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication(username, password);
                    }
                });

    }

    public Properties defaultMailConfig() {

        Properties defaultMailConfig = new Properties();

        defaultMailConfig.put(MAIL_SMTP_AUTH, config.getProperty(MAIL_SMTP_AUTH);
        defaultMailConfig.put(MAIL_SMTP_STARTTLS_ENABLE,
                config.getProperty(MAIL_SMTP_STARTTLS_ENABLE);
        defaultMailConfig.put(MAIL_SMTP_HOST, config.getProperty(MAIL_SMTP_HOST);
        defaultMailConfig.put(MAIL_SMTP_PORT, config.getProperty(MAIL_SMTP_PORT);
        defaultMailConfig.put(MAIL_SMTP_SSL_SOCKETFACTORY, mailSslSocketFactory());
        return defaultMailConfig;
    }

    private MailSSLSocketFactory mailSslSocketFactory() {
        MailSSLSocketFactory sf = null;
        try {
            sf = new MailSSLSocketFactory();
        } catch (GeneralSecurityException e) {
            LOGGER.error(e.getMessage(), e);
        }
        sf.setTrustAllHosts(true); // If you'd like to trust only some SSL-certificates, create an array
        return sf;
    }  
/*
* Receiving email 
*/  
public List<message> getNewMessages() throws MessagingException {
        if (null == mailInbox || !mailInbox.isOpen()) {
            openInboxFolder();
        }
        List<message> result = Lists.newArrayList();
        final Message[] unreadMessages = mailInbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
        if (null != unreadMessages && unreadMessages.length > 0) {
            result = ImmutableList.copyOf(unreadMessages);
        }
        return result;
    }


private Store connectToMailServer() throws MessagingException {
        if (null == session) {
            session = Session.getDefaultInstance(defaultMailConfig());
        }
        if (null == store || !store.isConnected()) {
            store = session.getStore(serverProtocol);
            store.connect(serverHost, port, username, password);
        }
        return store;
    }

private void openInboxFolder() throws MessagingException {
        final Store store = connectToMailServer();
        final String inboxNameVariant1 = INBOX;
        final String inboxNameVariant2 = INBOX.toLowerCase();
        final String inboxNameVariant3 = INBOX.toUpperCase();
        Folder folder;
        if (null != (folder = store.getFolder(inboxNameVariant1)) || null != (folder = store
                .getFolder(inboxNameVariant2)) || null != (folder = store.getFolder
                (inboxNameVariant3))) {
            mailInbox = folder;
            mailInbox.open(Folder.READ_WRITE);
        }
    }

}

Stage 3. Notifying a user about undelivered email

In this stage we will:

  • Receive a server reply announcing undelivered email
  • Count the number of server replies
  • Generate sending of email with your chosen content to specified addresses

To implement the algorithm we'll need to define three methods, one each for:

  • Retrieving unread messages from the server. (At Azoft, our server is used only for sending messages, so there will be no replies received. If your server is used for receiving as well, you may want to use a filter specifying something such as all messages with 'Undelivered Email' in the subject line, etc.)

This is method getNewMessages() from MailService class

  • Counting undelivered email
public int countMessages() throws MessagingException{
	return getNewMessages().size(); // You could also add a field for the inbox list
}
  • Creating and sending messages
//Builder pattern
public class MailMessage {

    private static final String MESSAGE_TEXT_CONTENT_PARAMS = "text/plain; charset=UTF-8";

    private final Message message;

    private final String subject;
    private final String text;
    private final DataSource attachment;
    private final String from;
    private final String recipients;

    public static class Builder {

        private final Message message;

        private String subject;
        private String text;
        private DataSource attachment;
        private String from;
        private String recipients;

        public Builder(Message message) {
            this.message = message;
        }

        public Builder from(String from) {
            this.from = from;
            return this;
        }

        public Builder recipients(String recipients) {
            this.recipients = recipients;
            return this;
        }

        public Builder subject(String subject) {
            this.subject = subject;
            return this;
        }

        public Builder text(String text) {
            this.text = text;
            return this;
        }

        public Builder attachment(DataSource attachment) {
            this.attachment = attachment;
            return this;
        }

        public Message build() throws MessagingException {
            message.setFrom(new InternetAddress(from));
            message.setRecipients(Message.RecipientType.TO,
                    InternetAddress.parse(recipients));
            String subject = this.subject.replace('\n', ' ');
            message.setSubject(subject);
            message.setContent(createBodyPart());

            return message;
        }

        private Multipart createBodyPart() throws MessagingException {
            Multipart multipart = new MimeMultipart();
            multipart.addBodyPart(createTextPart());
            if (null != attachment) {
                multipart.addBodyPart(createAttacmentPart());
            }

            return multipart;

        }

        private BodyPart createTextPart() throws MessagingException {
            BodyPart messageBody = new MimeBodyPart();
            messageBody.setContent(text, MESSAGE_TEXT_CONTENT_PARAMS);

            return messageBody;
        }

        private BodyPart createAttacmentPart() throws MessagingException {
            BodyPart attachmentPart = new MimeBodyPart();
            attachmentPart.setDataHandler(new DataHandler(attachment));
            attachmentPart.setFileName(attachment.getName());

            return attachmentPart;

        }

    }

    private MailMessage(Builder builder) {
        this.message = builder.message;
        this.from = builder.from;
        this.recipients = builder.recipients;
        this.subject = builder.subject;
        this.text = builder.text;
        this.attachment = builder.attachment;
    }

}

It’s also a good idea to send several test emails to known incorrect addresses to ensure that everything is working fine.

Stage 4. Retrieving message content information

Using regular expressions is the best way for selecting an information block from text. In our case this approach has both an advantage and disadvantage. The advantage is that an accurate regular expression guarantees that you are selecting the right block, while the disadvantage is that regular expressions aren’t universal and can process only predefined information, i.e., if code is changed, it may be a sign of a virus.

We mentioned above that all messages in a system should use a universal format. What interests us here is the message subject. Let's choose its structure.

The message sent by the system will contain:

  •  a ‘certid’ retrieved from the subject line of undelivered email error messages.

In this case, the following regular expressions will be used:

public static final Pattern CERTIFICATE_SUBJECT_PATTERN = Pattern.compile("(Subject: Certificate #)\\w{8}", Pattern.DOTALL);

Now we'll write several methods we'll need to work with the message.

  • search for ‘certid’ — a recursive method

All undelivered email consists of three parts. Usually, the most important information — date, subject, To: and From: fields — is contained in the third part. All three parts have different formats.

The code below searches and selects the necessary information from the message:

private static final String MIME_TYPE_MULTIPART = "multipart/*";
    private static final String MIME_TYPE_RFC_822 = "message/rfc822";


public String findCertId(Part p) throws MessagingException, IOException {
        return p instanceof Message ? findMimeMessageCertId(p) : findNonMimeMessageCertId(p);
    }

    private String findMimeMessageCertId(Part p) throws MessagingException {
        Message m = (Message) p;
        String subject = m.getSubject();
        return extractCertidFromEmailSubject(subject);
    }

    private String findNonMimeMessageCertId(Part p) throws MessagingException, IOException {
        String certid = null;
        if (p.isMimeType(MIME_TYPE_MULTIPART)) {
            Multipart mp = (Multipart) p.getContent();
            int count = mp.getCount();
            for (int i = 0; i < count; i++) {
                return findCertId(mp.getBodyPart(i));
            }
        } else if (p.isMimeType(MIME_TYPE_RFC_822)) {
            certid = findCertId((Part) p.getContent());
        }

        return certid;
    }

private static String extractCertidFromEmailSubject(String subject) {
        String certid = null;
        if (!Strings.isNullOrEmpty(subject)) {
            Matcher certidMatcher = CERTID_FROM_SUBJECT_PATTERN.matcher(subject);
            if (certidMatcher.find()) {
                certid = certidMatcher.group(1).trim().toUpperCase();
            }
        }

        return certid;
    }

Alternative solutions

In the process of working on the task we came across two interesting alternative solutions:

com.sun.mail.dsn.jar — Another library for processing undelivered email, it worked fine with Gmail but for some reason it did not function with the mail server used by our app.

VERP — This solution seemed too complicated and we preferred to solve the task using a more simple, universal method understood by developers with varied programming backgrounds.

VN:F [1.9.22_1171]
Rating: 4.2/5 (5 votes cast)
VN:F [1.9.22_1171]
Rating: +2 (from 2 votes)
Implementing Notifications on Undelivered Email Using javax.mail and Regular Expressions, 4.2 out of 5 based on 5 ratings

Content created by Eugene Ustimenko