mailbox.CreationViaEmail.java Source code

Java tutorial

Introduction

Here is the source code for mailbox.CreationViaEmail.java

Source

/**
 * Yobi, Project Hosting SW
 *
 * Copyright 2014 NAVER Corp.
 * http://yobi.io
 *
 * @Author Yi EungJun
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package mailbox;

import com.sun.mail.imap.IMAPMessage;
import com.googlecode.htmlcompressor.compressor.HtmlCompressor;
import mailbox.exceptions.IssueNotFound;
import mailbox.exceptions.MailHandlerException;
import mailbox.exceptions.PermissionDenied;
import mailbox.exceptions.PostingNotFound;
import models.*;
import models.enumeration.ResourceType;
import models.resource.Resource;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import play.Logger;
import play.api.i18n.Lang;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import utils.AccessControl;
import utils.MimeType;

import javax.annotation.Nonnull;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.internet.*;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 * A set of methods to create a resource from an incoming email.
 */
public class CreationViaEmail {
    /**
     * Create a comment from the given email.
     *
     * @param message
     * @param target
     * @throws MessagingException
     * @throws MailHandlerException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    @Transactional
    public static Comment saveComment(IMAPMessage message, Resource target)
            throws MessagingException, MailHandlerException, IOException, NoSuchAlgorithmException {
        User author = IMAPMessageUtil.extractSender(message);

        if (!AccessControl.isProjectResourceCreatable(author, target.getProject(), target.getType())) {
            throw new PermissionDenied(cannotCreateMessage(author, target.getProject(), target.getType()));
        }

        Content parsedMessage = extractContent(message);

        Comment comment = makeNewComment(target, author, parsedMessage.body);

        comment.save();

        Map<String, Attachment> relatedAttachments = saveAttachments(parsedMessage.attachments,
                comment.asResource());

        if (new ContentType(parsedMessage.type).match(MimeType.HTML)) {
            comment.contents = postprocessForHTML(comment.contents, relatedAttachments);
            comment.update();
        }

        new OriginalEmail(message.getMessageID(), comment.asResource()).save();

        // Add the event
        addEvent(NotificationEvent.forNewComment(comment, author), message.getAllRecipients(), author);

        return comment;
    }

    /**
     * Does postprocessing for HTML document.
     *
     * 1. Replaces cid with attachments.
     * 2. Removes newlines between HTML tags which will make the result rendered
     *    by markdown ugly.
     *
     * @param contents
     * @param relatedAttachments
     * @return
     */
    private static String postprocessForHTML(String contents, Map<String, Attachment> relatedAttachments) {
        return new HtmlCompressor().compress(replaceCidWithAttachments(contents, relatedAttachments));
    }

    private static Comment makeNewComment(Resource target, User sender, String body)
            throws IssueNotFound, PostingNotFound {
        Comment comment;
        Long id = Long.valueOf(target.getId());

        switch (target.getType()) {
        case ISSUE_POST:
            Issue issue = Issue.finder.byId(id);
            if (issue == null) {
                throw new IssueNotFound(id);
            }
            comment = new IssueComment(issue, sender, body);
            break;
        case BOARD_POST:
            Posting posting = Posting.finder.byId(id);
            if (posting == null) {
                throw new PostingNotFound(id);
            }
            comment = new PostingComment(posting, sender, body);
            break;
        default:
            throw new IllegalArgumentException("Unsupported resource type: " + target.getType());
        }

        return comment;
    }

    /**
     * Create an issue from the given email.
     *
     * @param message
     * @param project
     * @throws MessagingException
     * @throws PermissionDenied
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    static Issue saveIssue(IMAPMessage message, Project project)
            throws MessagingException, PermissionDenied, IOException, NoSuchAlgorithmException {
        User sender = IMAPMessageUtil.extractSender(message);
        if (!AccessControl.isProjectResourceCreatable(sender, project, ResourceType.ISSUE_POST)) {
            throw new PermissionDenied(cannotCreateMessage(sender, project, ResourceType.ISSUE_POST));
        }
        Content parsedMessage = extractContent(message);
        String messageId = message.getMessageID();
        Address[] recipients = message.getAllRecipients();
        String subject = message.getSubject();

        return saveIssue(subject, project, sender, parsedMessage, messageId, recipients);
    }

    @Transactional
    public static Issue saveIssue(String subject, Project project, User sender, Content parsedMessage,
            String messageId, Address[] recipients)
            throws MessagingException, IOException, NoSuchAlgorithmException {
        Issue issue = new Issue(project, sender, subject, parsedMessage.body);
        issue.save();

        Map<String, Attachment> relatedAttachments = saveAttachments(parsedMessage.attachments, issue.asResource());

        if (new ContentType(parsedMessage.type).match(MimeType.HTML)) {
            issue.refresh();
            issue.body = postprocessForHTML(issue.body, relatedAttachments);
            issue.update();
        }

        new OriginalEmail(messageId, issue.asResource()).save();

        // Add the event
        NotificationEvent event = NotificationEvent.forNewIssue(issue, sender);
        addEvent(event, recipients, sender);

        return issue;
    }

    /**
     * Create a review comment from the given email.
     *
     * @param message
     * @param target
     * @throws IOException
     * @throws MessagingException
     * @throws PermissionDenied
     * @throws NoSuchAlgorithmException
     */
    static void saveReviewComment(IMAPMessage message, Resource target)
            throws IOException, MessagingException, PermissionDenied, NoSuchAlgorithmException {
        User sender = IMAPMessageUtil.extractSender(message);

        if (!AccessControl.isProjectResourceCreatable(sender, target.getProject(), ResourceType.REVIEW_COMMENT)) {
            throw new PermissionDenied(cannotCreateMessage(sender, target.getProject(), target.getType()));
        }

        Content content = extractContent(message);
        String messageID = message.getMessageID();
        Address[] allRecipients = message.getAllRecipients();

        saveReviewComment(target, sender, content, messageID, allRecipients);
    }

    @Transactional
    protected static ReviewComment saveReviewComment(Resource target, User sender, Content content,
            String messageID, Address[] allRecipients)
            throws MessagingException, IOException, NoSuchAlgorithmException {
        ReviewComment comment;
        CommentThread thread = CommentThread.find.byId(Long.valueOf(target.getId()));

        if (thread == null) {
            throw new IllegalArgumentException();
        }

        comment = new ReviewComment();
        comment.setContents(content.body);
        comment.author = new UserIdent(sender);
        comment.thread = thread;
        comment.save();

        Map<String, Attachment> relatedAttachments = saveAttachments(content.attachments, comment.asResource());

        if (new ContentType(content.type).match(MimeType.HTML)) {
            // replace cid with attachments
            comment.setContents(replaceCidWithAttachments(comment.getContents(), relatedAttachments));
            comment.update();
        }

        new OriginalEmail(messageID, comment.asResource()).save();

        // Add the event
        if (thread.isOnPullRequest()) {
            addEvent(NotificationEvent.forNewComment(sender, thread.pullRequest, comment), allRecipients, sender);
        } else {
            try {
                String commitId;

                if (thread instanceof CodeCommentThread) {
                    commitId = ((CodeCommentThread) thread).commitId;
                } else if (thread instanceof NonRangedCodeCommentThread) {
                    commitId = ((NonRangedCodeCommentThread) thread).commitId;
                } else {
                    throw new IllegalArgumentException();
                }

                addEvent(NotificationEvent.forNewCommitComment(target.getProject(), comment, commitId, sender),
                        allRecipients, sender);
            } catch (Exception e) {
                Logger.warn("Failed to send a notification", e);
            }
        }

        return comment;
    }

    // You don't need to instantiate this class because this class is just
    // a set of static methods.
    private CreationViaEmail() {
    }

    @Nonnull
    private static Content extractContent(MimePart part) throws IOException, MessagingException {
        return processPart(part, null);
    }

    @Nonnull
    private static Content processPart(MimePart part, MimePart parent) throws MessagingException, IOException {
        if (part == null) {
            return new Content();
        }

        if (part.getFileName() != null) {
            // Assume that a part which has a filename is an attachment.
            return new Content(part);
        }

        if (part.isMimeType("text/*")) {
            return getContent(part);
        } else if (part.isMimeType("multipart/*")) {
            if (part.isMimeType(MimeType.MULTIPART_RELATED)) {
                return getContentWithAttachments(part);
            } else if (part.isMimeType(MimeType.MULTIPART_ALTERNATIVE)) {
                return getContentOfBestPart(part, parent);
            } else {
                return getJoinedContent(part);
            }
        }

        return new Content();
    }

    private static Content getJoinedContent(MimePart part) throws IOException, MessagingException {
        Content result = new Content();
        MimeMultipart mp = (MimeMultipart) part.getContent();
        for (int i = 0; i < mp.getCount(); i++) {
            MimeBodyPart p = (MimeBodyPart) mp.getBodyPart(i);
            result.merge(processPart(p, part));
        }
        return result;
    }

    private static Content getContent(MimePart part) throws IOException, MessagingException {
        Content result = new Content();
        result.body = (String) part.getContent();
        result.type = part.getContentType();
        return result;
    }

    private static Content getContentOfBestPart(MimePart part, MimePart parent)
            throws IOException, MessagingException {
        MimeBodyPart best = null;
        MimeMultipart mp = (MimeMultipart) part.getContent();
        for (int i = 0; i < mp.getCount(); i++) {
            // Prefer HTML if the parent is a multipart/related part which may contain
            // inline images, because text/plain cannot embed the images.
            boolean isHtmlPreferred = parent != null && parent.isMimeType(MimeType.MULTIPART_RELATED);
            best = better((MimeBodyPart) mp.getBodyPart(i), best, isHtmlPreferred);
        }
        return processPart(best, part);
    }

    private static Content getContentWithAttachments(MimePart part) throws MessagingException, IOException {
        Content result = new Content();
        String rootId = new ContentType(part.getContentType()).getParameter("start");
        MimeMultipart mp = (MimeMultipart) part.getContent();
        for (int i = 0; i < mp.getCount(); i++) {
            MimePart p = (MimePart) mp.getBodyPart(i);
            if (isRootPart(p, i, rootId)) {
                result = result.merge(processPart(p, part));
            } else {
                result.attachments.add(p);
            }
        }
        return result;
    }

    /**
     * Returns true if the given part is root part.
     *
     * The given part is root part, if the part is the first one and the given
     * root id is not defined or the content id of the part equals to the given
     * root id.
     *
     * @param part
     * @param nthPart
     * @param rootId
     * @return
     * @throws MessagingException
     */
    private static boolean isRootPart(MimePart part, int nthPart, String rootId) throws MessagingException {
        return (rootId == null && nthPart == 0) || StringUtils.equals(part.getContentID(), rootId);
    }

    private static int getPoint(BodyPart p, String[] preferences) throws MessagingException {
        if (p == null) {
            return 0;
        }

        for (int i = 0; i < preferences.length; i++) {
            if (p.isMimeType(preferences[i])) {
                return preferences.length + 1 - i;
            }
        }

        return 1;
    }

    /**
     * multipart/related > text/plain > the others
     *
     * @param p
     * @param best
     * @param isHtmlPreferred
     * @return
     * @throws javax.mail.MessagingException
     */
    private static MimeBodyPart better(MimeBodyPart p, MimeBodyPart best, boolean isHtmlPreferred)
            throws MessagingException {
        String[] preferences;
        if (isHtmlPreferred) {
            preferences = new String[] { MimeType.MULTIPART_RELATED, MimeType.HTML, MimeType.PLAIN_TEXT };
        } else {
            preferences = new String[] { MimeType.MULTIPART_RELATED, MimeType.PLAIN_TEXT, MimeType.HTML };
        }

        return getPoint(p, preferences) > getPoint(best, preferences) ? p : best;
    }

    private static String cannotCreateMessage(User user, Project project, ResourceType resourceType) {
        Lang lang = Lang.apply(user.getPreferredLanguage());
        String resourceTypeName = resourceType.getName(lang);
        return Messages.get(lang, "viaEmail.error.cannotCreate", user, resourceTypeName, project);
    }

    private static void addEvent(NotificationEvent event, Address[] recipients, User sender) {
        HashSet<User> emailUsers = new HashSet<>();
        emailUsers.add(sender);
        for (Address addr : recipients) {
            emailUsers.add(User.findByEmail(((InternetAddress) addr).getAddress()));
        }
        event.receivers.removeAll(emailUsers);
        NotificationEvent.add(event);
    }

    private static String replaceCidWithAttachments(String html, Map<String, Attachment> attachments) {
        Document doc = Jsoup.parse(html);
        String[] attrNames = { "src", "href" };

        for (String attrName : attrNames) {
            Elements tags = doc.select("*[" + attrName + "]");
            for (Element tag : tags) {
                String uriString = tag.attr(attrName).trim();

                if (!uriString.toLowerCase().startsWith("cid:")) {
                    continue;
                }

                String cid = uriString.substring("cid:".length());

                if (!attachments.containsKey(cid)) {
                    continue;
                }

                Long id = attachments.get(cid).id;
                tag.attr(attrName, controllers.routes.AttachmentApp.getFile(id).url());
            }
        }

        Elements bodies = doc.getElementsByTag("body");

        if (bodies.size() > 0) {
            return bodies.get(0).html();
        } else {
            return doc.html();
        }
    }

    private static Attachment saveAttachment(Part partToAttach, Resource container)
            throws MessagingException, IOException, NoSuchAlgorithmException {
        Attachment attach = new Attachment();
        String fileName = MimeUtility.decodeText(partToAttach.getFileName());
        attach.store(partToAttach.getInputStream(), fileName, container);
        if (!attach.mimeType.equalsIgnoreCase(partToAttach.getContentType())) {
            Logger.info("The email says the content type is '" + partToAttach.getContentType()
                    + "' but Yobi determines it is '" + attach.mimeType + "'");
        }

        return attach;
    }

    private static Map<String, Attachment> saveAttachments(Collection<MimePart> partsToAttach, Resource container)
            throws MessagingException, IOException, NoSuchAlgorithmException {
        Map<String, Attachment> result = new HashMap<>();

        for (MimePart partToAttach : partsToAttach) {
            Attachment attachment = saveAttachment(partToAttach, container);
            if (partToAttach.getContentID() != null) {
                String cid = partToAttach.getContentID().trim();
                cid = cid.replace("<", "");
                cid = cid.replace(">", "");
                result.put(cid, attachment);
            }
        }

        return result;
    }

}