mitm.application.djigzo.james.matchers.SubjectPhoneNumber.java Source code

Java tutorial

Introduction

Here is the source code for mitm.application.djigzo.james.matchers.SubjectPhoneNumber.java

Source

/*
 * Copyright (c) 2009-2011, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, 
 * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, 
 * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, 
 * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Eclipse Public License, 
 * tyrex license, freemarker license, dom4j license, mx4j license,
 * Spice Software License, Common Development and Distribution License
 * (CDDL), Common Public License (CPL) the licensors of this Program grant 
 * you additional permission to convey the resulting work.
 */
package mitm.application.djigzo.james.matchers;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import mitm.application.djigzo.User;
import mitm.application.djigzo.james.DjigzoMailAttributes;
import mitm.application.djigzo.james.DjigzoMailAttributesImpl;
import mitm.application.djigzo.james.MailAddressUtils;
import mitm.application.djigzo.james.MessageOriginatorIdentifier;
import mitm.application.djigzo.service.SystemServices;
import mitm.application.djigzo.workflow.UserWorkflow;
import mitm.application.djigzo.workflow.UserWorkflow.GetUserMode;
import mitm.common.hibernate.DatabaseAction;
import mitm.common.hibernate.DatabaseActionExecutor;
import mitm.common.hibernate.DatabaseActionExecutorBuilder;
import mitm.common.hibernate.DatabaseException;
import mitm.common.hibernate.SessionManager;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.util.CollectionUtils;
import mitm.common.util.PhoneNumberUtils;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Matcher that can detect a telephone number in the subject. If a telephone number is found
 * the telephone number of the recipient is set to the subject telephone number. if there are 
 * multiple recipient and the subject contains a telephone number the user telephone number
 * property is not set because it's impossible to match the number with the correct recipient. 
 * 
 * The telephone number is only detected if the subject ends with the telephone number (unless
 * the default telephone regular expression has been overridden).
 * 
 * Examples of valid subjects with a telephone number:
 * 
 * "test 123456"
 * "other +1234567   "
 * "hi! +(800)12(34)56"
 * 
 * Examples of invalid subjects with a telephone number:
 * 
 * "some subject"
 * "Hi 123"
 * "Hi 12345678 some text"
 * 
 * @author Martijn Brinkers
 *
 */
public class SubjectPhoneNumber extends AbstractContextAwareDjigzoMatcher {
    private final static Logger logger = LoggerFactory.getLogger(SubjectPhoneNumber.class);

    /*
     * Default expression used to detect a phone number in the subject. This only matches on
     * lines where the line ends with the telehpone number.
     * 
     * Matching examples:
     * 
     * "test 123456"
     * "other +1234567   "
     * "hi! +(800)12(34)56"
     * 
     * Non matching examples:
     * 
     * "some subject"
     * "Hi 123"
     * "Hi 12345678 some text"
     */
    private final static String DEFAULT_PHONE_NUMBER_EXPRESSION = "(\\+|\\s|^)([\\s\\-()]*\\d){6,25}\\s*$";

    /*
     * The number of times a database action should be retried when a ConstraintViolation occurs
     */
    private final static int ACTION_RETRIES = 3;

    /*
     * Pattern used to remove all non numbers
     */
    private final static Pattern phoneCleanPattern = Pattern.compile("[^0-9]*");

    /*
     * The pattern which is matched against the subject
     */
    private Pattern phoneNumberPattern;

    /*
     * manages database sessions
     */
    private SessionManager sessionManager;

    /*
     * Used for adding and retrieving users
     */
    private UserWorkflow userWorkflow;

    /*
     * Used to execute database actions in a transaction
     */
    private DatabaseActionExecutor actionExecutor;

    /*
     * Is used to identify the 'sender' (from or enveloped sender or...) of the message
     */
    private MessageOriginatorIdentifier messageOriginatorIdentifier;

    /*
     * The key under which we store the activation context
     */
    protected static final String ACTIVATION_CONTEXT_KEY = "phoneNumber";

    @Override
    protected Logger getLogger() {
        return logger;
    }

    protected String getSubjectExpression() {
        String expression = getCondition();

        if (StringUtils.isBlank(expression)) {
            expression = DEFAULT_PHONE_NUMBER_EXPRESSION;
        }

        return expression;
    }

    @Override
    public void init() {
        String expression = getSubjectExpression();

        if (expression == null || "".equals(expression)) {
            throw new IllegalArgumentException("Expression is missing.");
        }

        phoneNumberPattern = Pattern.compile(expression);

        StrBuilder sb = new StrBuilder();

        sb.append("phoneNumberPattern: ");
        sb.append(phoneNumberPattern);

        getLogger().info(sb.toString());

        sessionManager = SystemServices.getSessionManager();

        userWorkflow = SystemServices.getUserWorkflow();

        messageOriginatorIdentifier = SystemServices.getMessageOriginatorIdentifier();

        actionExecutor = DatabaseActionExecutorBuilder.createDatabaseActionExecutor(sessionManager);

        assert (actionExecutor != null);
    }

    private void setRecipientPhoneNumber(MailAddress recipient, String phoneNumber)
            throws AddressException, HierarchicalPropertiesException {
        String email = EmailAddressUtils.canonicalizeAndValidate(recipient.toInternetAddress().getAddress(), true);

        if (email == null) {
            /*
             * Should not happen because MailAddress should always be a valid email address
             */
            logger.warn(recipient + " is not a valid email address.");

            return;
        }

        User user = userWorkflow.getUser(email, GetUserMode.CREATE_IF_NOT_EXIST);

        user.getUserPreferences().getProperties().setSMSPhoneNumber(phoneNumber);

        userWorkflow.makePersistent(user);
    }

    private String cleanPhoneNumber(String phoneNumber, String defaultCountryCode) {
        if (StringUtils.isBlank(phoneNumber)) {
            return null;
        }
        /*
         * We will assume that the regular expression used to match the phone number only matched
         * valid phone numbers. We will remove all non number characters
         */
        phoneNumber = phoneCleanPattern.matcher(phoneNumber).replaceAll("");

        /*
         * If a phone number starts with 0 we will add the default country code
         */
        phoneNumber = PhoneNumberUtils.addCountryCode(phoneNumber, defaultCountryCode);

        phoneNumber = PhoneNumberUtils.filterAndValidatePhoneNumber(phoneNumber);

        return phoneNumber;
    }

    private boolean addPhoneNumberAction(final Mail mail, MailAddress recipient, String phoneNumber)
            throws MessagingException, HierarchicalPropertiesException {
        String defaultCountryCode = null;

        User user = null;

        InternetAddress originator = messageOriginatorIdentifier.getOriginator(mail);

        if (originator != null) {
            user = userWorkflow.getUser(originator.getAddress(), GetUserMode.CREATE_IF_NOT_EXIST);
        }

        if (user != null) {
            defaultCountryCode = user.getUserPreferences().getProperties().getPhoneDefaultCountryCode();
        }

        phoneNumber = cleanPhoneNumber(phoneNumber, defaultCountryCode);

        boolean added = false;

        if (StringUtils.isNotEmpty(phoneNumber)) {
            setRecipientPhoneNumber(recipient, phoneNumber);

            added = true;
        } else {
            logger.warn("Phone number is missing.");
        }

        return added;
    }

    private boolean addPhoneNumber(final Mail mail, final MailAddress recipient, final String phoneNumber)
            throws DatabaseException {
        /*
         * addPhoneNumbersAction will be executed in a transaction. Under 'normal' circumstances this should never
         * result in a ConstraintViolationException. But, when a new user is added at the same time this action is executed
         * it can result in a ConstraintViolationException We therefore retry at max. COMMIT_RETRIES times when a 
         * ConstraintViolationException occurs.
         */
        return actionExecutor.executeTransaction(new DatabaseAction<Boolean>() {
            @Override
            public Boolean doAction(Session session) throws DatabaseException {
                Session previousSession = sessionManager.getSession();

                sessionManager.setSession(session);

                try {
                    return addPhoneNumberAction(mail, recipient, phoneNumber);
                } catch (MessagingException e) {
                    throw new DatabaseException(e);
                } catch (HierarchicalPropertiesException e) {
                    throw new DatabaseException(e);
                } finally {
                    sessionManager.setSession(previousSession);
                }
            }
        }, ACTION_RETRIES);
    }

    @Override
    public Collection<MailAddress> matchMail(Mail mail) throws MessagingException {
        List<MailAddress> matchingRecipients = null;

        String phoneNumber = null;

        MimeMessage message = mail.getMessage();

        /*
         * We need to check whether the original subject is stored in the mail attributes because
         * the check for the telephone number should be done on the original subject. The reason
         * for this is that the current subject can result in the detection of an incorrect
         * telephone number because the subject was changed because the subject trigger was
         * removed. 
         * 
         * Example:
         *  
         * original subject is: "test 123 [encrypt] 123456". The subject is: "test 123 123456" after
         * the trigger has been removed. The telephone nr 123123456 is detected instead of 123456.
         * 
         * See bug report: https://jira.djigzo.com/browse/GATEWAY-11
         * 
         */
        DjigzoMailAttributes mailAttributes = new DjigzoMailAttributesImpl(mail);

        String originalSubject = mailAttributes.getOriginalSubject();

        if (originalSubject == null) {
            originalSubject = message.getSubject();
        }

        if (StringUtils.isNotBlank(originalSubject)) {
            Matcher matcher = phoneNumberPattern.matcher(originalSubject);

            if (matcher.find()) {
                phoneNumber = matcher.group();

                /*
                 * Remove the match and set the subject without the number. 
                 * 
                 * Note: the match should be removed from the current subject!!
                 * 
                 * Note2: using substringBeforeLast is only correct if the pattern matches the end
                 * of the subject (which is the default). If the pattern matches something in the
                 * middle, substringBeforeLast should not be used.
                 */
                message.setSubject(StringUtils.substringBeforeLast(message.getSubject(), phoneNumber));
            }
        }

        if (StringUtils.isNotBlank(phoneNumber)) {
            matchingRecipients = MailAddressUtils.getRecipients(mail);

            int nrOfRecipients = CollectionUtils.getSize(matchingRecipients);

            if (nrOfRecipients == 1) {
                try {
                    boolean added = addPhoneNumber(mail, matchingRecipients.get(0), phoneNumber);

                    if (!added) {
                        /* 
                         * addPhoneNumbers has more thorough checks to see if the phone 
                         * number is valid and if not, there will be no matcj
                         */
                        matchingRecipients = Collections.emptyList();
                    }
                } catch (DatabaseException e) {
                    throw new MessagingException("Exception adding phone numbers.", e);
                }
            } else {
                /*
                 * A telephone number was found but we cannot add the number to a users account
                 * because we do not known to which recipient the number belongs. We will however
                 * return all the recipients.
                 */
                logger.warn("Found " + nrOfRecipients + " recipients but only one is supported.");
            }
        }

        if (matchingRecipients == null) {
            matchingRecipients = Collections.emptyList();
        }

        return matchingRecipients;
    }
}