Java tutorial
/* * 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; } }