mitm.common.pdf.MessagePDFBuilder.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.pdf.MessagePDFBuilder.java

Source

/*
 * Copyright (c) 2008-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.common.pdf;

import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.activation.DataHandler;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import javax.mail.internet.MimePart;
import javax.mail.util.ByteArrayDataSource;

import mitm.common.mail.BodyPartUtils;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.mail.HeaderUtils;
import mitm.common.mail.MimeTypes;
import mitm.common.mail.MimeUtils;
import mitm.common.util.StringReplaceUtils;
import mitm.common.util.URIUtils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.ColumnText;
import com.lowagie.text.pdf.FontSelector;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfFileSpecification;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;

/**
 * Creates a PDF that looks like an email from a MimeMessage. Attachments are included as well.
 * 
 * @author Martijn Brinkers
 *
 */
public class MessagePDFBuilder {
    private final static Logger logger = LoggerFactory.getLogger(MessagePDFBuilder.class);

    /*
     * The text used for the body when there is no body text found. Can happen when the message is HTML only.
     */
    private final static String MISSING_BODY = "*** Missing message body. Make sure the email is sent as text only "
            + "or sent as html with an alternative text part ***";

    private final static String FOOTER_TEXT = "Created with DJIGZO";

    /*
     * Pattern so we can create clickable links in the pdf.
     */
    private Pattern urlPattern = Pattern.compile(URIUtils.HTTP_URL_REG_EXPR);

    /*
     * Size of the document.
     */
    private float documentWidth = PageSize.A4.getWidth();
    private float documentHeight = PageSize.A4.getHeight();

    /*
     * The margins of the document.
     */
    private float marginLeft = 10;
    private float marginRight = 10;
    private float marginTop = 10;
    private float marginBottom = 10;

    /*
     * The default preferences of the PDF viewer when the PDF is opened.
     */
    private Set<ViewerPreference> viewerPreferences = new HashSet<ViewerPreference>();

    /*
     * Provides extra fonts
     */
    private FontProvider fontProvider;

    /*
     * If true everything is compressed (only supported by PDF 1.5).
     */
    private boolean fullCompression = false;

    /*
     * The document background gray fill.
     */
    private float grayFill = 0.9f;

    /*
     * PDF does not have tabs so we need to convert tabs to spaces.
     */
    private int tabWidth;

    /*
     * PDF document creator will be Djigzo.
     */
    private static final String CREATOR = "Djigzo";

    private float bodyFontSize = 10f;

    private float linkFontSize = 10f;

    private float headerFontSize = 12f;

    public MessagePDFBuilder() {
        initDefaultViewerPreferences();
    }

    @Override
    protected MessagePDFBuilder clone() {
        MessagePDFBuilder clone = new MessagePDFBuilder();

        clone.fontProvider = this.fontProvider;
        clone.documentWidth = this.documentWidth;
        clone.documentHeight = this.documentHeight;
        clone.marginLeft = this.marginLeft;
        clone.marginRight = this.marginRight;
        clone.marginTop = this.marginTop;
        clone.fullCompression = this.fullCompression;
        clone.grayFill = this.grayFill;
        clone.tabWidth = this.tabWidth;
        clone.bodyFontSize = this.bodyFontSize;
        clone.linkFontSize = this.linkFontSize;
        clone.headerFontSize = this.headerFontSize;

        clone.viewerPreferences.addAll(this.viewerPreferences);

        return clone;
    }

    private void initDefaultViewerPreferences() {
        viewerPreferences.add(ViewerPreference.PAGE_MODE_USE_ATTACHMENTS);
        viewerPreferences.add(ViewerPreference.DISPLAY_DOC_TITLE);
    }

    public void setViewerPreference(Set<ViewerPreference> viewerPreferences) {
        this.viewerPreferences = viewerPreferences;
    }

    private Document createDocument() throws DocumentException {
        Rectangle pageSize = new Rectangle(documentWidth, documentHeight);

        pageSize.setGrayFill(grayFill);

        Document document = new Document(pageSize, marginLeft, marginRight, marginTop, marginBottom);

        document.addCreator(CREATOR);

        return document;
    }

    private int getViewerPreferencesIntValue() {
        int intValue = 0;

        for (ViewerPreference preference : viewerPreferences) {
            intValue = intValue | preference.intValue();
        }

        return intValue;
    }

    private PdfWriter createPdfWriter(Document document, OutputStream pdfStream) throws DocumentException {
        PdfWriter pdfWriter = PdfWriter.getInstance(document, pdfStream);

        if (fullCompression) {
            pdfWriter.setFullCompression();
        }

        pdfWriter.setViewerPreferences(getViewerPreferencesIntValue());

        return pdfWriter;
    }

    private void addAttachment(final Part part, PdfWriter pdfWriter) throws IOException, MessagingException {
        byte[] content = IOUtils.toByteArray(part.getInputStream());

        String filename = StringUtils
                .defaultString(HeaderUtils.decodeTextQuietly(MimeUtils.getFilenameQuietly(part)), "attachment.bin");

        String baseType;

        String contentType = null;

        try {
            contentType = part.getContentType();

            MimeType mimeType = new MimeType(contentType);

            baseType = mimeType.getBaseType();
        } catch (MimeTypeParseException e) {
            /*
             * Can happen when the content-type is not correct. Example with missing ; between charset and name:
             * 
             * Content-Type: application/pdf;
             *      charset="Windows-1252" name="postcard2010.pdf"
             */
            logger.warn("Unable to infer MimeType from content type. Fallback to application/octet-stream. "
                    + "Content-Type: " + contentType);

            baseType = MimeTypes.OCTET_STREAM;
        }

        PdfFileSpecification fileSpec = PdfFileSpecification.fileEmbedded(pdfWriter, null, filename, content,
                true /* compress */, baseType, null);

        pdfWriter.addFileAttachment(fileSpec);
    }

    private void addAttachments(PdfWriter pdfWriter, Collection<Part> attachments)
            throws MessagingException, IOException {
        for (Part part : attachments) {
            addAttachment(part, pdfWriter);
        }
    }

    private void addTextPart(String text, Phrase bodyPhrase, FontSelector fontSelector) {
        if (StringUtils.isNotEmpty(text)) {
            bodyPhrase.add(fontSelector.process(StringUtils.defaultString(text)));
        }
    }

    private void addLinkPart(String link, Phrase bodyPhrase, Font linkFont) {
        String linkName = link;

        link = link.trim();

        Chunk anchor = new Chunk(linkName, linkFont);

        /*
         * A anchor need http (or https)
         */
        if (!link.toLowerCase().startsWith("http")) {
            link = "http://" + link;
        }

        anchor.setAnchor(link);

        bodyPhrase.add(anchor);
    }

    private Font createLinkFont() {
        /*
         * Font for anchors (links)
         */
        Font linkFont = new Font();
        linkFont.setStyle(Font.UNDERLINE);
        linkFont.setColor(0, 0, 255);
        linkFont.setSize(linkFontSize);

        return linkFont;
    }

    private Font createHeaderFont() {
        /*
         * Font for the headers
         */
        Font headerFont = new Font();
        headerFont.setStyle(Font.BOLD);
        headerFont.setSize(headerFontSize);

        return headerFont;
    }

    private FontSelector createBodyFontSelector() {
        return createFontSelector(FontFactory.HELVETICA, bodyFontSize, Font.NORMAL, Color.BLACK);
    }

    private FontSelector createHeaderFontSelector() {
        return createFontSelector(FontFactory.HELVETICA, headerFontSize, Font.BOLD, Color.BLACK);
    }

    private FontSelector createFontSelector(String fontName, float size, int style, Color color) {
        FontSelector selector = new FontSelector();

        selector.addFont(FontFactory.getFont(fontName, size, style, color));

        /*
         * Add extra fonts provided by the FontProvider
         */
        if (fontProvider != null) {
            Collection<Font> extraFonts = fontProvider.getFonts();

            for (Font extraFont : extraFonts) {
                selector.addFont(extraFont);
            }
        }

        /*
         * Supported CJK (Chinese, Japanse, Korean)
         * 
         * his is the list of fonts supported in the iTextAsian.jar:
         * Chinese Simplified:
         *      STSong-Light and STSongStd-Light with the encodings UniGB-UCS2-H and UniGB-UCS2-V
         * Chinese Traditional:
         *      MHei-Medium, MSung-Light and MSungStd-Light with the encodings UniCNS-UCS2-H and UniCNS-UCS2-V
         * Japanese:
         *      HeiseiMin-W3, HeiseiKakuGo-W5 and KozMinPro-Regular with the encodings UniJIS-UCS2-H, UniJIS-UCS2-V, UniJIS-UCS2-HW-H and UniJIS-UCS2-HW-V
         * Korean:
         *      HYGoThic-Medium, HYSMyeongJo-Medium and HYSMyeongJoStd with the encodings UniKS-UCS2-H and UniKS-UCS2-V
         * 
         * Need to find out which fonts we should add to the selector and in which order
         */
        selector.addFont(FontFactory.getFont("KozMinPro-Regular", "UniJIS-UCS2-H", BaseFont.NOT_EMBEDDED, size,
                style, color));
        selector.addFont(
                FontFactory.getFont("MSung-Light", "UniCNS-UCS2-H", BaseFont.NOT_EMBEDDED, size, style, color));
        selector.addFont(
                FontFactory.getFont("HYGoThic-Medium", "UniKS-UCS2-H", BaseFont.NOT_EMBEDDED, size, style, color));

        return selector;
    }

    private void addBodyAndAttachments(PdfWriter pdfWriter, Document document, PdfPTable bodyTable, String body,
            Collection<Part> attachments) throws DocumentException, MessagingException, IOException {
        /*
         * Font for anchors (links)
         */
        Font linkFont = createLinkFont();

        FontSelector bodyFontSelector = createBodyFontSelector();

        PdfPCell bodyCell = new PdfPCell();

        /*
         * Body table will be white
         */
        bodyCell.setGrayFill(1f);

        bodyCell.setPadding(10);

        Phrase bodyPhrase = new Phrase();

        bodyCell.setPhrase(bodyPhrase);

        /*
         * Matcher we need to convert links to clickable links
         */
        Matcher urlMatcher = urlPattern.matcher(body);

        String textPart;

        int currentIndex = 0;

        /*
         * Add body and links 
         */
        while (urlMatcher.find()) {
            textPart = body.substring(currentIndex, urlMatcher.start());

            addTextPart(textPart, bodyPhrase, bodyFontSelector);

            String linkPart = urlMatcher.group();

            if (linkPart != null) {
                addLinkPart(linkPart, bodyPhrase, linkFont);

                currentIndex = urlMatcher.start() + linkPart.length();
            }
        }

        textPart = body.substring(currentIndex);

        addTextPart(textPart, bodyPhrase, bodyFontSelector);

        bodyTable.addCell(bodyCell);

        document.add(bodyTable);

        addAttachments(pdfWriter, attachments);
    }

    private String getFilename(Part part, String defaultIfNull) {
        String filename = null;

        filename = HeaderUtils.decodeTextQuietly(MimeUtils.getFilenameQuietly(part));

        if (filename == null) {
            try {
                filename = part.getDescription();
            } catch (MessagingException e) {
            }

            if (filename == null) {
                filename = defaultIfNull;
            }
        }

        return filename;
    }

    private String getAttachmentHeader(Collection<Part> attachments) {
        StrBuilder sb = new StrBuilder(256);

        for (Part attachment : attachments) {
            String filename = getFilename(attachment, "unknown");

            sb.append(filename);
            sb.appendSeparator("; ");
        }

        return sb.toString();
    }

    private Part convertRFC822(Part attachment) {
        try {
            Object o = attachment.getContent();

            if (o instanceof MimeMessage) {
                MimeMessage message = (MimeMessage) o;

                MessagePDFBuilder pdfBuilder = this.clone();

                ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();

                pdfBuilder.buildPDF(message, null, pdfStream);

                MimePart pdfPart = new MimeBodyPart();

                pdfPart.setDataHandler(
                        new DataHandler(new ByteArrayDataSource(pdfStream.toByteArray(), "application/pdf")));

                String filename = getFilename(attachment, "message.pdf");

                if (!filename.toLowerCase().endsWith(".pdf")) {
                    filename = filename + ".pdf";
                }

                pdfPart.setFileName(filename);

                return pdfPart;
            }
        } catch (IOException e) {
            logger.error("Error trying to converting to RFC822.");
        } catch (MessagingException e) {
            logger.error("Error trying to converting to RFC822.");
        } catch (DocumentException e) {
            logger.error("Error trying to converting to RFC822.");
        }

        return attachment;
    }

    private Collection<Part> preprocessAttachments(final Collection<Part> attachments) {
        Collection<Part> prepared = new LinkedList<Part>();

        for (Part attachment : attachments) {
            try {
                if (attachment.isMimeType("message/rfc822")) {
                    /*
                     * We need to convert the attached message to a PDF.
                     */
                    Part pdfAttachment = convertRFC822(attachment);

                    prepared.add(pdfAttachment);
                } else {
                    prepared.add(attachment);
                }
            } catch (MessagingException e) {
                prepared.add(attachment);
            }
        }

        return prepared;
    }

    private void addReplyLink(Document document, String replyURL) throws DocumentException {
        PdfPTable replyTable = new PdfPTable(1);
        replyTable.setWidthPercentage(100f);

        replyTable.setSplitLate(false);

        replyTable.setSpacingBefore(5f);
        replyTable.setHorizontalAlignment(Element.ALIGN_LEFT);

        Font linkFont = new Font();

        linkFont.setStyle(Font.BOLD);
        linkFont.setColor(0, 0, 255);
        linkFont.setSize(headerFontSize);

        Chunk anchor = new Chunk("Reply", linkFont);

        anchor.setAnchor(replyURL);

        Phrase phrase = new Phrase();

        phrase.add(anchor);

        PdfPCell cell = new PdfPCell(phrase);
        cell.setBorder(Rectangle.NO_BORDER);

        replyTable.addCell(cell);

        document.add(replyTable);
    }

    public void buildPDF(MimeMessage message, String replyURL, OutputStream pdfStream)
            throws DocumentException, MessagingException, IOException {
        Document document = createDocument();

        PdfWriter pdfWriter = createPdfWriter(document, pdfStream);

        document.open();

        String[] froms = null;

        try {
            froms = EmailAddressUtils.addressesToStrings(message.getFrom(), true /* mime decode */);
        } catch (MessagingException e) {
            logger.warn("From address is not a valid email address.");
        }

        if (froms != null) {
            for (String from : froms) {
                document.addAuthor(from);
            }
        }

        String subject = null;

        try {
            subject = message.getSubject();
        } catch (MessagingException e) {
            logger.error("Error getting subject.", e);
        }

        if (subject != null) {
            document.addSubject(subject);
            document.addTitle(subject);
        }

        String[] tos = null;

        try {
            tos = EmailAddressUtils.addressesToStrings(message.getRecipients(RecipientType.TO),
                    true /* mime decode */);
        } catch (MessagingException e) {
            logger.warn("To is not a valid email address.");
        }

        String[] ccs = null;

        try {
            ccs = EmailAddressUtils.addressesToStrings(message.getRecipients(RecipientType.CC),
                    true /* mime decode */);
        } catch (MessagingException e) {
            logger.warn("CC is not a valid email address.");
        }

        Date sentDate = null;

        try {
            sentDate = message.getSentDate();
        } catch (MessagingException e) {
            logger.error("Error getting sent date.", e);
        }

        Collection<Part> attachments = new LinkedList<Part>();

        String body = BodyPartUtils.getPlainBodyAndAttachments(message, attachments);

        attachments = preprocessAttachments(attachments);

        if (body == null) {
            body = MISSING_BODY;
        }

        /*
         * PDF does not have tab support so we convert tabs to spaces
         */
        body = StringReplaceUtils.replaceTabsWithSpaces(body, tabWidth);

        PdfPTable headerTable = new PdfPTable(2);

        headerTable.setHorizontalAlignment(Element.ALIGN_LEFT);
        headerTable.setWidthPercentage(100);
        headerTable.setWidths(new int[] { 1, 6 });
        headerTable.getDefaultCell().setBorder(Rectangle.NO_BORDER);

        Font headerFont = createHeaderFont();

        FontSelector headerFontSelector = createHeaderFontSelector();

        PdfPCell cell = new PdfPCell(new Paragraph("From:", headerFont));
        cell.setBorder(Rectangle.NO_BORDER);
        cell.setHorizontalAlignment(Element.ALIGN_RIGHT);

        headerTable.addCell(cell);

        String decodedFroms = StringUtils.defaultString(StringUtils.join(froms, ", "));

        headerTable.addCell(headerFontSelector.process(decodedFroms));

        cell = new PdfPCell(new Paragraph("To:", headerFont));
        cell.setBorder(Rectangle.NO_BORDER);
        cell.setHorizontalAlignment(Element.ALIGN_RIGHT);

        headerTable.addCell(cell);
        headerTable.addCell(headerFontSelector.process(StringUtils.defaultString(StringUtils.join(tos, ", "))));

        cell = new PdfPCell(new Paragraph("CC:", headerFont));
        cell.setBorder(Rectangle.NO_BORDER);
        cell.setHorizontalAlignment(Element.ALIGN_RIGHT);

        headerTable.addCell(cell);
        headerTable.addCell(headerFontSelector.process(StringUtils.defaultString(StringUtils.join(ccs, ", "))));

        cell = new PdfPCell(new Paragraph("Subject:", headerFont));
        cell.setBorder(Rectangle.NO_BORDER);
        cell.setHorizontalAlignment(Element.ALIGN_RIGHT);

        headerTable.addCell(cell);
        headerTable.addCell(headerFontSelector.process(StringUtils.defaultString(subject)));

        cell = new PdfPCell(new Paragraph("Date:", headerFont));
        cell.setBorder(Rectangle.NO_BORDER);
        cell.setHorizontalAlignment(Element.ALIGN_RIGHT);

        headerTable.addCell(cell);
        headerTable.addCell(ObjectUtils.toString(sentDate));

        cell = new PdfPCell(new Paragraph("Attachments:", headerFont));
        cell.setBorder(Rectangle.NO_BORDER);
        cell.setHorizontalAlignment(Element.ALIGN_RIGHT);

        headerTable.addCell(cell);
        headerTable
                .addCell(headerFontSelector.process(StringUtils.defaultString(getAttachmentHeader(attachments))));

        document.add(headerTable);

        if (replyURL != null) {
            addReplyLink(document, replyURL);
        }

        /*
         * Body table will contain the body of the message
         */
        PdfPTable bodyTable = new PdfPTable(1);
        bodyTable.setWidthPercentage(100f);

        bodyTable.setSplitLate(false);

        bodyTable.setSpacingBefore(15f);
        bodyTable.setHorizontalAlignment(Element.ALIGN_LEFT);

        addBodyAndAttachments(pdfWriter, document, bodyTable, body, attachments);

        Phrase footer = new Phrase(FOOTER_TEXT);

        PdfContentByte cb = pdfWriter.getDirectContent();

        ColumnText.showTextAligned(cb, Element.ALIGN_RIGHT, footer, document.right(), document.bottom(), 0);

        document.close();
    }

    public FontProvider getFontProvider() {
        return fontProvider;
    }

    public void setFontProvider(FontProvider fontProvider) {
        this.fontProvider = fontProvider;
    }
}