org.obm.push.mail.EmailManager.java Source code

Java tutorial

Introduction

Here is the source code for org.obm.push.mail.EmailManager.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: GPL 2.0
 *
 * The contents of this file are subject to the GNU General Public
 * License Version 2 or later (the "GPL").
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Initial Developer of the Original Code is
 *   MiniG.org project members
 *
 * ***** END LICENSE BLOCK ***** */

package org.obm.push.mail;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.columba.ristretto.message.Address;
import org.minig.imap.FastFetch;
import org.minig.imap.Flag;
import org.minig.imap.FlagsList;
import org.minig.imap.IMAPException;
import org.minig.imap.ListInfo;
import org.minig.imap.ListResult;
import org.minig.imap.SearchQuery;
import org.minig.imap.StoreClient;
import org.obm.configuration.EmailConfiguration;
import org.obm.locator.store.LocatorService;
import org.obm.push.bean.BackendSession;
import org.obm.push.bean.Email;
import org.obm.push.bean.MSEmail;
import org.obm.push.bean.SyncState;
import org.obm.push.exception.DaoException;
import org.obm.push.exception.SendEmailException;
import org.obm.push.exception.SmtpInvalidRcptException;
import org.obm.push.exception.activesync.ProcessingEmailException;
import org.obm.push.exception.activesync.StoreEmailException;
import org.obm.push.mail.smtp.SmtpSender;
import org.obm.push.service.EventService;
import org.obm.push.store.EmailDao;
import org.obm.push.utils.DateUtils;
import org.obm.push.utils.FileUtils;
import org.obm.sync.client.calendar.AbstractEventSyncClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;

@Singleton
public class EmailManager implements IEmailManager {

    private static final Logger logger = LoggerFactory.getLogger(EmailManager.class);

    private final EmailDao emailDao;
    private final SmtpSender smtpProvider;
    private final LocatorService locatorService;
    private final EmailSync emailSync;
    private final boolean loginWithDomain;
    private final boolean activateTLS;
    private final EventService eventService;

    @Inject
    /*package*/ EmailManager(EmailDao emailDao, EmailConfiguration emailConfiguration, SmtpSender smtpSender,
            EmailSync emailSync, LocatorService locatorService, EventService eventService) {

        this.emailSync = emailSync;
        this.smtpProvider = smtpSender;
        this.emailDao = emailDao;
        this.locatorService = locatorService;
        this.eventService = eventService;
        this.loginWithDomain = emailConfiguration.loginWithDomain();
        this.activateTLS = emailConfiguration.activateTls();
    }

    @Override
    public String locateImap(BackendSession bs) {
        String locateImap = locatorService.getServiceLocation("mail/imap_frontend", bs.getLoginAtDomain());
        logger.info("Using {} as imap host.", locateImap);
        return locateImap;
    }

    private StoreClient getImapClient(BackendSession bs) {
        final String imapHost = locateImap(bs);
        final String login = getLogin(bs);
        StoreClient storeClient = new StoreClient(imapHost, 143, login, bs.getPassword());

        logger.debug("Creating storeClient with login {} : " + "loginWithDomain = {} | activateTLS = {}",
                new Object[] { login, loginWithDomain, activateTLS });

        return storeClient;
    }

    private String getLogin(BackendSession bs) {
        String login = bs.getLoginAtDomain();
        if (!loginWithDomain) {
            int at = login.indexOf("@");
            if (at > 0) {
                login = login.substring(0, at);
            }
        }
        return login;
    }

    @Override
    public MailChanges getSync(BackendSession bs, SyncState syncState, Integer deviceId, Integer collectionId,
            String collectionName) throws IMAPException, DaoException {

        StoreClient store = getImapClient(bs);
        try {
            login(store);
            store.select(parseMailBoxName(store, collectionName));
            return emailSync.getSync(store, deviceId, syncState, collectionId);
        } finally {
            store.logout();
        }
    }

    @Override
    public List<MSEmail> fetchMails(BackendSession bs, AbstractEventSyncClient calendarClient, Integer collectionId,
            String collectionName, Collection<Long> uids) throws IMAPException {

        final List<MSEmail> mails = new LinkedList<MSEmail>();
        final StoreClient store = getImapClient(bs);
        try {
            login(store);
            store.select(parseMailBoxName(store, collectionName));

            final MailMessageLoader mailLoader = new MailMessageLoader(store, calendarClient, eventService);
            for (final Long uid : uids) {
                final MSEmail email = mailLoader.fetch(collectionId, uid, bs);
                if (email != null) {
                    mails.add(email);
                }
            }
        } finally {
            store.logout();
        }
        return mails;
    }

    private ListResult listAllFolder(StoreClient store) {
        return store.listAll();
    }

    @Override
    public void updateReadFlag(BackendSession bs, String collectionName, Long uid, boolean read)
            throws IMAPException {
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            String mailBoxName = parseMailBoxName(store, collectionName);
            store.select(mailBoxName);
            FlagsList fl = new FlagsList();
            fl.add(Flag.SEEN);
            store.uidStore(Arrays.asList(uid), fl, read);
            logger.info("flag  change: " + (read ? "+" : "-") + " SEEN" + " on mail " + uid + " in " + mailBoxName);
        } finally {
            store.logout();
        }
    }

    @Override
    public String parseMailBoxName(BackendSession bs, String collectionName) throws IMAPException {
        // parse obm:\\adrien@test.tlse.lng\email\INBOX\Sent
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            return parseMailBoxName(store, collectionName);
        } finally {
            store.logout();
        }
    }

    private String parseMailBoxName(StoreClient store, String collectionName) throws IMAPException {
        if (collectionName.toLowerCase().endsWith(EmailConfiguration.IMAP_INBOX_NAME.toLowerCase())) {
            return EmailConfiguration.IMAP_INBOX_NAME;
        }

        int slash = collectionName.lastIndexOf("email\\");
        final String boxName = collectionName.substring(slash + "email\\".length());
        final ListResult lr = listAllFolder(store);
        for (final ListInfo i : lr) {
            if (i.getName().toLowerCase().contains(boxName.toLowerCase())) {
                return i.getName();
            }
        }
        throw new IMAPException("Cannot find IMAP folder for collection [ " + collectionName + " ]");
    }

    @Override
    public void delete(BackendSession bs, Integer devId, String collectionPath, Integer collectionId, Long uid)
            throws IMAPException, DaoException {

        StoreClient store = getImapClient(bs);
        try {
            login(store);
            String mailBoxName = parseMailBoxName(store, collectionPath);
            store.select(mailBoxName);
            FlagsList fl = new FlagsList();
            fl.add(Flag.DELETED);
            logger.info("delete conv id = ", uid);
            store.uidStore(Arrays.asList(uid), fl, true);
            store.expunge();
            deleteEmails(devId, collectionId, Arrays.asList(uid));
        } finally {
            store.logout();
        }
    }

    @Override
    public Long moveItem(BackendSession bs, Integer devId, String srcFolder, Integer srcFolderId, String dstFolder,
            Integer dstFolderId, Long uid) throws IMAPException, DaoException {

        StoreClient store = getImapClient(bs);
        Collection<Long> newUid = null;
        try {
            login(store);
            String srcMailBox = parseMailBoxName(store, srcFolder);
            String dstMailBox = parseMailBoxName(store, dstFolder);
            store.select(srcMailBox);
            List<Long> uids = Arrays.asList(uid);
            newUid = store.uidCopy(uids, dstMailBox);
            FlagsList fl = new FlagsList();
            fl.add(Flag.DELETED);
            logger.info("delete conv id = ", uid);
            store.uidStore(uids, fl, true);
            store.expunge();
            deleteEmails(devId, srcFolderId, Arrays.asList(uid));
            addMessageInCache(store, devId, dstFolderId, uid);
        } finally {
            store.logout();
        }
        if (newUid == null || newUid.isEmpty()) {
            return null;
        }
        return newUid.iterator().next();
    }

    @Override
    public List<InputStream> fetchMIMEMails(BackendSession bs, AbstractEventSyncClient calendarClient,
            String collectionName, Set<Long> uids) throws IMAPException {

        List<InputStream> mails = new LinkedList<InputStream>();
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            store.select(parseMailBoxName(store, collectionName));
            for (Long uid : uids) {
                mails.add(store.uidFetchMessage(uid));
            }
        } finally {
            store.logout();
        }
        return mails;
    }

    private void login(StoreClient store) throws IMAPException {
        if (!store.login(activateTLS)) {
            throw new IMAPException("Cannot log into imap server");
        }
    }

    @Override
    public void setAnsweredFlag(BackendSession bs, String collectionName, Long uid) throws IMAPException {
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            String mailBoxName = parseMailBoxName(store, collectionName);
            store.select(mailBoxName);
            FlagsList fl = new FlagsList();
            fl.add(Flag.ANSWERED);
            store.uidStore(Arrays.asList(uid), fl, true);
            logger.info("flag  change : ANSWERED on mail {} in {}", new Object[] { uid, mailBoxName });
        } finally {
            store.logout();
        }
    }

    @Override
    public void sendEmail(BackendSession bs, Address from, Set<Address> setTo, Set<Address> setCc,
            Set<Address> setCci, InputStream mimeMail, Boolean saveInSent)
            throws ProcessingEmailException, SendEmailException, SmtpInvalidRcptException, StoreEmailException {

        SmtpInvalidRcptException invalidRctp = null;
        InputStream streamMail = null;
        try {
            streamMail = new ByteArrayInputStream(FileUtils.streamBytes(mimeMail, true));
            streamMail.mark(streamMail.available());

            try {
                smtpProvider.sendEmail(bs, from, setTo, setCc, setCci, streamMail);
            } catch (SmtpInvalidRcptException e1) {
                invalidRctp = e1;
            }

            if (saveInSent) {
                streamMail.reset();
                final Long uid = storeInSent(bs, streamMail);
                if (uid != null) {
                    logger.info("This mail {} is stored in 'sent' folder.", uid);
                } else {
                    logger.error("The mail can't to be store in 'sent' folder.");
                }
            }

        } catch (IOException e) {
            throw new ProcessingEmailException(e);
        } finally {
            closeStream(streamMail);
        }

        if (invalidRctp != null) {
            throw invalidRctp;
        }
    }

    private void closeStream(InputStream mimeMail) {
        if (mimeMail != null) {
            try {
                mimeMail.close();
            } catch (IOException t) {
                logger.error(t.getMessage(), t);
            }
        }
    }

    @Override
    public InputStream findAttachment(BackendSession bs, String collectionName, Long mailUid,
            String mimePartAddress) throws IMAPException {
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            String mailBoxName = parseMailBoxName(store, collectionName);
            store.select(mailBoxName);
            return store.uidFetchPart(mailUid, mimePartAddress);
        } finally {
            store.logout();
        }
    }

    @Override
    public void purgeFolder(BackendSession bs, Integer devId, String collectionPath, Integer collectionId)
            throws IMAPException, DaoException {
        long time = System.currentTimeMillis();
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            String mailBoxName = parseMailBoxName(store, collectionPath);
            store.select(mailBoxName);
            logger.info("Mailbox folder[ {} ] will be purged...", collectionPath);
            Collection<Long> uids = store.uidSearch(new SearchQuery());
            FlagsList fl = new FlagsList();
            fl.add(Flag.DELETED);
            store.uidStore(uids, fl, true);
            store.expunge();
            deleteEmails(devId, collectionId, uids);
            time = System.currentTimeMillis() - time;
            logger.info("Mailbox folder[ {} ] was purged in {} millisec. {} messages have been deleted",
                    new Object[] { collectionPath, time, uids.size() });
        } finally {
            store.logout();
        }
    }

    @Override
    public Long storeInInbox(BackendSession bs, InputStream mailContent, boolean isRead)
            throws StoreEmailException {
        logger.info("Store mail in folder[Inbox]");
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            return storeMail(store, EmailConfiguration.IMAP_INBOX_NAME, isRead, mailContent, false);
        } catch (IMAPException e) {
            throw new StoreEmailException("Error during store mail in Inbox folder", e);
        } finally {
            store.logout();
        }
    }

    /**
     * Store the mail in the Sent folder storeInSent reset the mimeMail will be
     * if storeInSent read it
     * 
     * @param bs the BackendSession
     * @param mail the mail that will be stored
     * @return the imap uid of the mail
     * @throws StoreEmailException
     */
    private Long storeInSent(BackendSession bs, InputStream mail) throws StoreEmailException {
        StoreClient store = getImapClient(bs);
        try {
            login(store);
            String sentFolderName = null;
            ListResult lr = listAllFolder(store);
            for (ListInfo i : lr) {
                if (i.getName().toLowerCase().endsWith("sent")) {
                    sentFolderName = i.getName();
                }
            }
            return storeMail(store, sentFolderName, true, mail, true);
        } catch (IMAPException e) {
            throw new StoreEmailException("Error during store mail in Sent folder", e);
        } finally {
            store.logout();
        }
    }

    /**
     * 
     * @param store
     *            the StoreClient
     * @param folderName
     *            the folder name where the mail will be stored
     * @param isRead
     *            if true the message will be stored with SEEN Flag
     * @param reset
     *            if true mailContent will be reseted
     * @return the imap uid of the mail
     */
    private Long storeMail(StoreClient store, String folderName, boolean isRead, InputStream mailContent,
            boolean reset) {
        Long ret = null;
        if (folderName != null) {
            if (reset && mailContent.markSupported()) {
                mailContent.mark(0);
            }
            FlagsList fl = new FlagsList();
            if (isRead) {
                fl.add(Flag.SEEN);
            }
            ret = store.append(folderName, mailContent, fl);
            store.expunge();
        }
        return ret;
    }

    private void deleteEmails(Integer devId, Integer collectionId, Collection<Long> mailUids) throws DaoException {
        try {
            emailDao.deleteSyncEmails(devId, collectionId, mailUids);
        } catch (DaoException e) {
            throw new DaoException("Error while deleting messages in db", e);
        }
    }

    private void addMessageInCache(StoreClient store, Integer devId, Integer collectionId, Long mailUids)
            throws DaoException {
        Collection<FastFetch> fetch = store.uidFetchFast(ImmutableSet.of(mailUids));
        Collection<Email> emails = Collections2.transform(fetch, new Function<FastFetch, Email>() {
            @Override
            public Email apply(FastFetch input) {
                return new Email(input.getUid(), input.isRead(), input.getInternalDate());
            }
        });
        try {
            markEmailsAsSynced(devId, collectionId, emails);
        } catch (DaoException e) {
            throw new DaoException("Error while adding messages in db", e);
        }
    }

    public void markEmailsAsSynced(Integer devId, Integer collectionId, Collection<Email> messages)
            throws DaoException {
        markEmailsAsSynced(devId, collectionId, DateUtils.getCurrentDate(), messages);
    }

    public void markEmailsAsSynced(Integer devId, Integer collectionId, Date lastSync, Collection<Email> emails)
            throws DaoException {
        Set<Email> allEmailsToMark = Sets.newHashSet(emails);
        Set<Email> alreadySyncedEmails = emailDao.alreadySyncedEmails(collectionId, devId, emails);
        Set<Email> modifiedEmails = findModifiedEmails(allEmailsToMark, alreadySyncedEmails);
        Set<Email> emailsNeverTrackedBefore = filterOutAlreadySyncedEmails(allEmailsToMark, alreadySyncedEmails);
        emailDao.updateSyncEntriesStatus(devId, collectionId, modifiedEmails);
        emailDao.createSyncEntries(devId, collectionId, emailsNeverTrackedBefore, lastSync);
    }

    private Set<Email> findModifiedEmails(Set<Email> allEmailsToMark, Set<Email> alreadySyncedEmails) {
        Map<Long, Email> indexedEmailsToMark = getEmailsAsUidTreeMap(allEmailsToMark);
        HashSet<Email> modifiedEmails = Sets.newHashSet();
        for (Email email : alreadySyncedEmails) {
            Email modifiedEmail = indexedEmailsToMark.get(email.getUid());
            if (modifiedEmail != null && !modifiedEmail.equals(email)) {
                modifiedEmails.add(modifiedEmail);
            }
        }
        return modifiedEmails;
    }

    private Set<Email> filterOutAlreadySyncedEmails(Set<Email> allEmailsToMark, Set<Email> alreadySyncedEmails) {
        return org.obm.push.utils.collection.Sets.difference(allEmailsToMark, alreadySyncedEmails,
                new Comparator<Email>() {
                    @Override
                    public int compare(Email o1, Email o2) {
                        return Long.valueOf(o1.getUid() - o2.getUid()).intValue();
                    }
                });
    }

    private Map<Long, Email> getEmailsAsUidTreeMap(Set<Email> emails) {
        return Maps.uniqueIndex(emails, new Function<Email, Long>() {
            @Override
            public Long apply(Email email) {
                return email.getIndex();
            }
        });
    }

    @Override
    public boolean getLoginWithDomain() {
        return loginWithDomain;
    }

    @Override
    public boolean getActivateTLS() {
        return activateTLS;
    }

    @Override
    public void updateData(Integer devId, Integer collectionId, Date lastSync, Collection<Long> removedEmailUids,
            Collection<Email> updatedEmails) throws DaoException {

        if (removedEmailUids != null && !removedEmailUids.isEmpty()) {
            emailDao.deleteSyncEmails(devId, collectionId, lastSync, removedEmailUids);
        }

        if (updatedEmails != null && !updatedEmails.isEmpty()) {
            markEmailsAsSynced(devId, collectionId, lastSync, updatedEmails);
        }
    }

}