19 package org.sleuthkit.autopsy.thunderbirdparser;
 
   21 import com.google.common.collect.Iterables;
 
   22 import com.pff.PSTAttachment;
 
   23 import com.pff.PSTException;
 
   24 import com.pff.PSTFile;
 
   25 import com.pff.PSTFolder;
 
   26 import com.pff.PSTMessage;
 
   28 import java.io.FileOutputStream;
 
   29 import java.io.IOException;
 
   30 import java.io.InputStream;
 
   31 import java.nio.ByteBuffer;
 
   32 import java.util.ArrayList;
 
   33 import java.util.Iterator;
 
   34 import java.util.List;
 
   35 import java.util.Scanner;
 
   36 import java.util.logging.Level;
 
   38 import org.openide.util.NbBundle;
 
   56     private static final Logger logger = Logger.getLogger(PstParser.class.getName());
 
   60     private static int PST_HEADER = 0x2142444E;
 
   62     private final IngestServices services;
 
   64     private PSTFile pstFile;
 
   67     private int failureCount = 0;
 
   69     private final List<String> errorList = 
new ArrayList<>();
 
   71     PstParser(IngestServices services) {
 
   72         this.services = services;
 
   95     ParseResult open(File file, 
long fileID) {
 
   97             return ParseResult.ERROR;
 
  101             pstFile = 
new PSTFile(file);
 
  102         } 
catch (PSTException ex) {
 
  105             if (ex.getMessage().equals(
"Only unencrypted and compressable PST files are supported at this time")) { 
 
  106                 logger.log(Level.INFO, 
"Found encrypted PST file."); 
 
  107                 return ParseResult.ENCRYPT;
 
  109             String msg = file.getName() + 
": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); 
 
  110             logger.log(Level.WARNING, msg, ex);
 
  111             return ParseResult.ERROR;
 
  112         } 
catch (IOException ex) {
 
  113             String msg = file.getName() + 
": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); 
 
  114             logger.log(Level.WARNING, msg, ex);
 
  115             return ParseResult.ERROR;
 
  116         } 
catch (IllegalArgumentException ex) { 
 
  117             logger.log(Level.INFO, 
"Found encrypted PST file."); 
 
  118             return ParseResult.ENCRYPT;
 
  121         return ParseResult.OK;
 
  130     Iterator<EmailMessage> getEmailMessageIterator() {
 
  131         if (pstFile == null) {
 
  135         Iterable<EmailMessage> iterable = null;
 
  138             iterable = getEmaiMessageIterator(pstFile.getRootFolder(), 
"\\", fileID, 
true);
 
  139         } 
catch (PSTException | IOException ex) {
 
  140             logger.log(Level.WARNING, String.format(
"Exception thrown while parsing fileID: %d", fileID), ex);
 
  143         if (iterable == null) {
 
  147         return iterable.iterator();
 
  156     List<EmailMessage> getPartialEmailMessages() {
 
  157         List<EmailMessage> messages = 
new ArrayList<>();
 
  158         Iterator<EmailMessage> iterator = getPartialEmailMessageIterator();
 
  159         if (iterator != null) {
 
  160             while (iterator.hasNext()) {
 
  161                 messages.add(iterator.next());
 
  175         for (String msg: errorList) {
 
  176             result += 
"<li>" + msg + 
"</li>"; 
 
  186     int getFailureCount() {
 
  197     private Iterator<EmailMessage> getPartialEmailMessageIterator() {
 
  198         if (pstFile == null) {
 
  202         Iterable<EmailMessage> iterable = null;
 
  205             iterable = getEmaiMessageIterator(pstFile.getRootFolder(), 
"\\", fileID, 
false);
 
  206         } 
catch (PSTException | IOException ex) {
 
  207             logger.log(Level.WARNING, String.format(
"Exception thrown while parsing fileID: %d", fileID), ex);
 
  210         if (iterable == null) {
 
  214         return iterable.iterator();
 
  231     private Iterable<EmailMessage> getEmaiMessageIterator(PSTFolder folder, String path, 
long fileID, 
boolean wholeMsg) 
throws PSTException, IOException {
 
  232         Iterable<EmailMessage> iterable = null;
 
  234         if (folder.getContentCount() > 0) {
 
  235             iterable = 
new PstEmailIterator(folder, path, fileID, wholeMsg).getIterable();
 
  238         if (folder.hasSubfolders()) {
 
  239             List<PSTFolder> subFolders = folder.getSubFolders();
 
  240             for (PSTFolder subFolder : subFolders) {
 
  241                 String newpath = path + 
"\\" + subFolder.getDisplayName();
 
  242                 Iterable<EmailMessage> subIterable = getEmaiMessageIterator(subFolder, newpath, fileID, wholeMsg);
 
  243                 if (subIterable == null) {
 
  247                 if (iterable != null) {
 
  248                     iterable = Iterables.concat(iterable, subIterable);
 
  250                     iterable = subIterable;
 
  267     private EmailMessage extractEmailMessage(PSTMessage msg, String localPath, 
long fileID) {
 
  268         EmailMessage email = 
new EmailMessage();
 
  269         email.setRecipients(msg.getDisplayTo());
 
  270         email.setCc(msg.getDisplayCC());
 
  271         email.setBcc(msg.getDisplayBCC());
 
  272         email.setSender(getSender(msg.getSenderName(), msg.getSenderEmailAddress()));
 
  273         email.setSentDate(msg.getMessageDeliveryTime());
 
  274         email.setTextBody(msg.getBody());
 
  275         if (
false == msg.getTransportMessageHeaders().isEmpty()) {
 
  276             email.setHeaders(
"\n-----HEADERS-----\n\n" + msg.getTransportMessageHeaders() + 
"\n\n---END HEADERS--\n\n");
 
  279         email.setHtmlBody(msg.getBodyHTML());
 
  282             rtf = msg.getRTFBody();
 
  283         } 
catch (PSTException | IOException ex) {
 
  284             logger.log(Level.INFO, 
"Failed to get RTF content from pst email."); 
 
  286         email.setRtfBody(rtf);
 
  287         email.setLocalPath(localPath);
 
  288         email.setSubject(msg.getSubject());
 
  289         email.setId(msg.getDescriptorNodeId());
 
  290         email.setMessageID(msg.getInternetMessageId());
 
  292         String inReplyToID = msg.getInReplyToId();
 
  293         email.setInReplyToID(inReplyToID);
 
  295         if (msg.hasAttachments()) {
 
  296             extractAttachments(email, msg, fileID);
 
  299         List<String> references = extractReferences(msg.getTransportMessageHeaders());
 
  300         if (inReplyToID != null && !inReplyToID.isEmpty()) {
 
  301             if (references == null) {
 
  302                 references = 
new ArrayList<>();
 
  303                 references.add(inReplyToID);
 
  304             } 
else if (!references.contains(inReplyToID)) {
 
  305                 references.add(inReplyToID);
 
  308         email.setReferences(references);
 
  320     private EmailMessage extractPartialEmailMessage(PSTMessage msg) {
 
  321         EmailMessage email = 
new EmailMessage();
 
  322         email.setSubject(msg.getSubject());
 
  323         email.setId(msg.getDescriptorNodeId());
 
  324         email.setMessageID(msg.getInternetMessageId());
 
  325         String inReplyToID = msg.getInReplyToId();
 
  326         email.setInReplyToID(inReplyToID);
 
  327         List<String> references = extractReferences(msg.getTransportMessageHeaders());
 
  328         if (inReplyToID != null && !inReplyToID.isEmpty()) {
 
  329             if (references == null) {
 
  330                 references = 
new ArrayList<>();
 
  331                 references.add(inReplyToID);
 
  332             } 
else if (!references.contains(inReplyToID)) {
 
  333                 references.add(inReplyToID);
 
  336         email.setReferences(references);
 
  347     @NbBundle.Messages({
"PstParser.noOpenCase.errMsg=Exception while getting open case."})
 
  348     private void extractAttachments(EmailMessage email, PSTMessage msg, 
long fileID) {
 
  349         int numberOfAttachments = msg.getNumberOfAttachments();
 
  350         String outputDirPath;
 
  352             outputDirPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator;
 
  353         } 
catch (NoCurrentCaseException ex) {
 
  354             logger.log(Level.SEVERE, 
"Exception while getting open case.", ex); 
 
  357         for (
int x = 0; x < numberOfAttachments; x++) {
 
  358             String filename = 
"";
 
  360                 PSTAttachment attach = msg.getAttachment(x);
 
  361                 long size = attach.getAttachSize();
 
  362                 long freeSpace = services.getFreeDiskSpace();
 
  363                 if ((freeSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && (size >= freeSpace)) {
 
  367                 filename = attach.getLongFilename();
 
  368                 if (filename.isEmpty()) {
 
  369                     filename = attach.getFilename();
 
  371                 String uniqueFilename = fileID + 
"-" + msg.getDescriptorNodeId() + 
"-" + attach.getContentId() + 
"-" + FileUtil.escapeFileName(filename);
 
  372                 String outPath = outputDirPath + uniqueFilename;
 
  373                 saveAttachmentToDisk(attach, outPath);
 
  375                 EmailMessage.Attachment attachment = 
new EmailMessage.Attachment();
 
  377                 long crTime = attach.getCreationTime() != null ? attach.getCreationTime().getTime() / 1000 : 0;
 
  378                 long mTime = attach.getModificationTime() != null ? attach.getModificationTime().getTime() / 1000 : 0;
 
  379                 String relPath = getRelModuleOutputPath() + File.separator + uniqueFilename;
 
  380                 attachment.setName(filename);
 
  381                 attachment.setCrTime(crTime);
 
  382                 attachment.setmTime(mTime);
 
  383                 attachment.setLocalPath(relPath);
 
  384                 attachment.setSize(attach.getFilesize());
 
  385                 attachment.setEncodingType(TskData.EncodingType.XOR1);
 
  386                 email.addAttachment(attachment);
 
  387             } 
catch (PSTException | IOException | NullPointerException ex) {
 
  393                         NbBundle.getMessage(
this.getClass(), 
"PstParser.extractAttch.errMsg.failedToExtractToDisk",
 
  395                 logger.log(Level.WARNING, 
"Failed to extract attachment from pst file.", ex); 
 
  396             } 
catch (NoCurrentCaseException ex) {
 
  397                 addErrorMessage(Bundle.PstParser_noOpenCase_errMsg());
 
  398                 logger.log(Level.SEVERE, Bundle.PstParser_noOpenCase_errMsg(), ex); 
 
  412     private void saveAttachmentToDisk(PSTAttachment attach, String outPath) 
throws IOException, PSTException {
 
  413         try (InputStream attachmentStream = attach.getFileInputStream();
 
  414                 EncodedFileOutputStream out = 
new EncodedFileOutputStream(
new FileOutputStream(outPath), TskData.EncodingType.XOR1)) {
 
  416             int bufferSize = 8176;
 
  417             byte[] buffer = 
new byte[bufferSize];
 
  418             int count = attachmentStream.read(buffer);
 
  421                 throw new IOException(
"attachmentStream invalid (read() fails). File " + attach.getLongFilename() + 
" skipped");
 
  424             while (count == bufferSize) {
 
  426                 count = attachmentStream.read(buffer);
 
  429                 byte[] endBuffer = 
new byte[count];
 
  430                 System.arraycopy(buffer, 0, endBuffer, 0, count);
 
  431                 out.write(endBuffer);
 
  444     private String getSender(String name, String addr) {
 
  445         if (name.isEmpty() && addr.isEmpty()) {
 
  447         } 
else if (name.isEmpty()) {
 
  449         } 
else if (addr.isEmpty()) {
 
  452             return name + 
": " + addr;
 
  463     public static boolean isPstFile(AbstractFile file) {
 
  464         byte[] buffer = 
new byte[4];
 
  466             int read = file.read(buffer, 0, 4);
 
  470             ByteBuffer bb = ByteBuffer.wrap(buffer);
 
  471             return bb.getInt() == PST_HEADER;
 
  472         } 
catch (TskCoreException ex) {
 
  473             logger.log(Level.WARNING, 
"Exception while detecting if a file is a pst file."); 
 
  483     private void addErrorMessage(String msg) {
 
  494     private List<String> extractReferences(String emailHeader) {
 
  495         Scanner scanner = 
new Scanner(emailHeader);
 
  496         StringBuilder buffer = null;
 
  497         while (scanner.hasNextLine()) {
 
  498             String token = scanner.nextLine();
 
  500             if (token.matches(
"^References:.*")) {
 
  501                 buffer = 
new StringBuilder();
 
  502                 buffer.append((token.substring(token.indexOf(
':') + 1)).trim());
 
  503             } 
else if (buffer != null) {
 
  504                 if (token.matches(
"^\\w+:.*$")) {
 
  505                     List<String> references = 
new ArrayList<>();
 
  506                     for (String 
id : buffer.toString().split(
">")) {
 
  507                         references.add(
id.trim() + 
">");
 
  511                     buffer.append(token.trim());
 
  541         PstEmailIterator(PSTFolder folder, String path, 
long fileID, 
boolean wholeMsg) {
 
  544             this.currentPath = path;
 
  547             if (folder.getContentCount() > 0) {
 
  549                     PSTMessage message = (PSTMessage) folder.getNextChild();
 
  550                     if (message != null) {
 
  552                             nextMsg = extractEmailMessage(message, currentPath, fileID);
 
  554                             nextMsg = extractPartialEmailMessage(message);
 
  557                 } 
catch (PSTException | IOException ex) {
 
  559                     logger.log(Level.WARNING, String.format(
"Unable to extract emails for path: %s file ID: %d ", path, fileID), ex);
 
  566             return nextMsg != null;
 
  575                 PSTMessage message = (PSTMessage) folder.getNextChild();
 
  576                 if (message != null) {
 
  578                         nextMsg = extractEmailMessage(message, currentPath, fileID);
 
  580                         nextMsg = extractPartialEmailMessage(message);
 
  585             } 
catch (PSTException | IOException ex) {
 
  586                 logger.log(Level.WARNING, String.format(
"Unable to extract emails for path: %s file ID: %d ", currentPath, fileID), ex);
 
  599         Iterable<EmailMessage> getIterable() {
 
  600             return new Iterable<EmailMessage>() {
 
  602                 public Iterator<EmailMessage> iterator() {