diff --git a/thunderbirdparser/ivy.xml b/thunderbirdparser/ivy.xml
index b66def4489..d2833c64c1 100644
--- a/thunderbirdparser/ivy.xml
+++ b/thunderbirdparser/ivy.xml
@@ -13,5 +13,6 @@
+
diff --git a/thunderbirdparser/nbproject/project.properties b/thunderbirdparser/nbproject/project.properties
index af5e0794ae..ea9d0786eb 100644
--- a/thunderbirdparser/nbproject/project.properties
+++ b/thunderbirdparser/nbproject/project.properties
@@ -1,13 +1,19 @@
+file.reference.apache-mime4j-core-0.8.0-SNAPSHOT.jar=release/modules/ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
+file.reference.apache-mime4j-dom-0.8.0-SNAPSHOT.jar=release/modules/ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
+file.reference.apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar=release/modules/ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar
file.reference.commons-lang3-3.8.1.jar=release/modules/ext/commons-lang3-3.8.1.jar
file.reference.apache-mime4j-core-0.8.0.jar=release/modules/ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
file.reference.apache-mime4j-dom-0.8.0.jar=release/modules/ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
file.reference.apache-mime4j-mbox-iterator-0.8.0.jar=release/modules/ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar
+file.reference.guava-19.0.jar=release/modules/ext/guava-19.0.jar
file.reference.java-libpst-1.0-SNAPSHOT.jar=release/modules/ext/java-libpst-1.0-SNAPSHOT.jar
file.reference.ez-vcard-0.10.5.jar=release/modules/ext/ez-vcard-0.10.5.jar
file.reference.vinnie-2.0.2.jar=release/modules/ext/vinnie-2.0.2.jar
javac.source=1.8
javac.compilerargs=-Xlint -Xlint:-serial
+javadoc.reference.guava-19.0.jar=release/modules/ext/guava-19.0-javadoc.jar
license.file=../LICENSE-2.0.txt
nbm.homepage=http://www.sleuthkit.org/autopsy/
nbm.needs.restart=true
+source.reference.guava-19.0.jar=release/modules/ext/guava-19.0-sources.jar
spec.version.base=4.0
diff --git a/thunderbirdparser/nbproject/project.xml b/thunderbirdparser/nbproject/project.xml
index d6e024f08c..c67ed7679f 100644
--- a/thunderbirdparser/nbproject/project.xml
+++ b/thunderbirdparser/nbproject/project.xml
@@ -54,18 +54,6 @@
ext/commons-lang3-3.8.1.jar
release/modules/ext/commons-lang3-3.8.1.jar
-
- ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
- release/modules/ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
-
-
- ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
- release/modules/ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
-
-
- ext/java-libpst-1.0-SNAPSHOT.jar
- release/modules/ext/java-libpst-1.0-SNAPSHOT.jar
-
ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar
release/modules/ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar
@@ -74,6 +62,22 @@
ext/ez-vcard-0.10.5.jar
release/modules/ext/ez-vcard-0.10.5.jar
+
+ ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
+ release/modules/ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
+
+
+ ext/java-libpst-1.0-SNAPSHOT.jar
+ release/modules/ext/java-libpst-1.0-SNAPSHOT.jar
+
+
+ ext/guava-19.0.jar
+ release/modules/ext/guava-19.0.jar
+
+
+ ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
+ release/modules/ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
+
ext/vinnie-2.0.2.jar
release/modules/ext/vinnie-2.0.2.jar
diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MboxParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MboxParser.java
index 42d74bcc2a..6604c2c623 100644
--- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MboxParser.java
+++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MboxParser.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2011-2014 Basis Technology Corp.
+ * Copyright 2011-2019 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,6 +33,7 @@ import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
@@ -57,22 +58,17 @@ import org.apache.tika.parser.txt.CharsetDetector;
import org.apache.tika.parser.txt.CharsetMatch;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
-import org.sleuthkit.autopsy.ingest.IngestServices;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.EncodedFileOutputStream;
/**
- * A parser that extracts information about email messages and attachments from
- * a mbox file.
- *
- * @author jwallace
+ * An Iterator for parsing mbox files. Wraps an instance of MBoxEmailIterator.
*/
-class MboxParser {
+class MboxParser implements Iterator{
private static final Logger logger = Logger.getLogger(MboxParser.class.getName());
- private DefaultMessageBuilder messageBuilder;
- private IngestServices services;
- private StringBuilder errors;
+ private final DefaultMessageBuilder messageBuilder;
+ private final StringBuilder errors;
/**
* The mime type string for html text.
@@ -83,9 +79,11 @@ class MboxParser {
* The local path of the mbox file.
*/
private String localPath;
+
+ private Iterator emailIterator = null;
+
- MboxParser(IngestServices services, String localPath) {
- this.services = services;
+ private MboxParser( String localPath) {
this.localPath = localPath;
messageBuilder = new DefaultMessageBuilder();
MimeConfig config = MimeConfig.custom().setMaxLineLen(-1).build();
@@ -97,63 +95,76 @@ class MboxParser {
static boolean isValidMimeTypeMbox(byte[] buffer) {
return (new String(buffer)).startsWith("From "); //NON-NLS
}
-
+
/**
- * Parse the mbox file and get the email messages.
- *
- * @param mboxFile
- *
- * @return a list of the email messages in the mbox file.
+ * Returns an instance of MBoxParser that will iterate and return EMailMessage
+ * objects with only the information needed for threading emails.
+ *
+ * @param localPath String path to the mboxFile
+ * @param mboxFile The mboxFile to parse
+ * @return Instance of MboxParser
*/
- List parse(File mboxFile, long fileID) {
+ static MboxParser getThreadInfoIterator(String localPath, File mboxFile) {
+ MboxParser parser = new MboxParser(localPath);
+ parser.createIterator(mboxFile, 0, false);
+ return parser;
+ }
+
+ /**
+ * Returns an instance of MBoxParser that will iterate "whole" EmailMessages.
+ *
+ * @param localPath String path to the mboxFile
+ * @param mboxFile The mboxFile to parse
+ * @param fileID The fileID of the abstractFile that mboxFile was found
+ * @return Instance of MboxParser
+ */
+ static MboxParser getEmailIterator(String localPath, File mboxFile, long fileID) {
+ MboxParser parser = new MboxParser(localPath);
+ parser.createIterator(mboxFile, fileID, true);
+
+ return parser;
+ }
+
+ /**
+ * Creates the real Iterator object instance.
+ *
+ * @param mboxFile The mboxFile to parse
+ * @param fileID The fileID of the abstractFile that mboxFile was found
+ * @param wholeMsg True if EmailMessage should have the whole message,
+ * not just the thread information.
+ */
+ private void createIterator(File mboxFile, long fileID, boolean wholeMsg) {
// Detect possible charsets
List encoders = getPossibleEncoders(mboxFile);
-
- CharsetEncoder theEncoder = null;
- Iterable mboxIterator = null;
+
// Loop through the possible encoders and find the first one that works.
// That will usually be one of the first ones.
for (CharsetEncoder encoder : encoders) {
try {
- mboxIterator = MboxIterator.fromFile(mboxFile).charset(encoder.charset()).build();
- theEncoder = encoder;
+ Iterable mboxIterable = MboxIterator.fromFile(mboxFile).charset(encoder.charset()).build();
+ if(mboxIterable != null) {
+ emailIterator = new MBoxEmailIterator(mboxIterable.iterator(), encoder, fileID, wholeMsg);
+ }
break;
} catch (CharConversionException | UnsupportedCharsetException ex) {
// Not the right encoder
} catch (IllegalArgumentException ex) {
// Not the right encoder
} catch (IOException ex) {
- logger.log(Level.WARNING, "couldn't find mbox file.", ex); //NON-NLS
+ logger.log(Level.WARNING, String.format("Failed to open mbox file: %s %d", mboxFile.getName(), fileID), ex); //NON-NLS
addErrorMessage(NbBundle.getMessage(this.getClass(), "MboxParser.parse.errMsg.failedToReadFile"));
- return new ArrayList<>();
}
}
-
- // If no encoders work, post an error message and return.
- if (mboxIterator == null || theEncoder == null) {
- addErrorMessage(NbBundle.getMessage(this.getClass(), "MboxParser.parse.errMsg.couldntFindCharset"));
- return new ArrayList<>();
- }
-
- List emails = new ArrayList<>();
- long failCount = 0;
-
- // Parse each message and extract an EmailMessage structure
- for (CharBufferWrapper message : mboxIterator) {
- try {
- Message msg = messageBuilder.parseMessage(message.asInputStream(theEncoder.charset()));
- emails.add(extractEmail(msg, fileID));
- } catch (RuntimeException | IOException ex) {
- logger.log(Level.WARNING, "Failed to get message from mbox: {0}", ex.getMessage()); //NON-NLS
- failCount++;
- }
- }
-
- if (failCount > 0) {
- addErrorMessage(
- NbBundle.getMessage(this.getClass(), "MboxParser.parse.errMsg.failedToParseNMsgs", failCount));
- }
- return emails;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return emailIterator != null ? emailIterator.hasNext() : false;
+ }
+
+ @Override
+ public EmailMessage next() {
+ return emailIterator != null ? emailIterator.next() : null;
}
String getErrors() {
@@ -211,6 +222,44 @@ class MboxParser {
return email;
}
+
+ /**
+ * Extract the subject, inReplyTo, message-ID and references from the Message
+ * object and returns them in a new EmailMessage object.
+ *
+ * @param msg Message object
+ *
+ * @return EmailMessage instance with only some of the message information
+ */
+ private EmailMessage extractPartialEmail(Message msg) {
+ EmailMessage email = new EmailMessage();
+ email.setSubject(msg.getSubject());
+ email.setMessageID(msg.getMessageId());
+
+ Field field = msg.getHeader().getField("in-reply-to"); //NON-NLS
+ String inReplyTo = null;
+
+ if (field != null) {
+ inReplyTo = field.getBody();
+ email.setInReplyToID(inReplyTo);
+ }
+
+ field = msg.getHeader().getField("references");
+ if (field != null) {
+ List references = new ArrayList<>();
+ for (String id : field.getBody().split(">")) {
+ references.add(id.trim() + ">");
+ }
+
+ if (!references.contains(inReplyTo)) {
+ references.add(inReplyTo);
+ }
+
+ email.setReferences(references);
+ }
+
+ return email;
+ }
/**
* Handle a multipart mime message. Recursively calls handleMultipart if one
@@ -333,7 +382,7 @@ class MboxParser {
addErrorMessage(
NbBundle.getMessage(this.getClass(),
"MboxParser.handleAttch.errMsg.failedToCreateOnDisk", outPath));
- logger.log(Level.INFO, "Failed to create file output stream for: " + outPath, ex); //NON-NLS
+ logger.log(Level.WARNING, "Failed to create file output stream for: " + outPath, ex); //NON-NLS
return;
}
@@ -346,14 +395,14 @@ class MboxParser {
// This could potentially be other types. Only seen this once.
}
} catch (IOException ex) {
- logger.log(Level.INFO, "Failed to write mbox email attachment to disk.", ex); //NON-NLS
+ logger.log(Level.WARNING, "Failed to write mbox email attachment to disk.", ex); //NON-NLS
addErrorMessage(NbBundle.getMessage(this.getClass(), "MboxParser.handleAttch.failedWriteToDisk", filename));
return;
} finally {
try {
fos.close();
} catch (IOException ex) {
- logger.log(Level.INFO, "Failed to close file output stream", ex); //NON-NLS
+ logger.log(Level.WARNING, "Failed to close file output stream", ex); //NON-NLS
}
}
@@ -441,7 +490,7 @@ class MboxParser {
try {
is.close();
} catch (IOException ex) {
- logger.log(Level.INFO, "Failed to close input stream"); //NON-NLS
+ logger.log(Level.WARNING, "Failed to close input stream"); //NON-NLS
}
}
}
@@ -449,4 +498,45 @@ class MboxParser {
private void addErrorMessage(String msg) {
errors.append("").append(msg).append(""); //NON-NLS
}
+
+ /**
+ * An Interator for mbox email messages.
+ */
+ final class MBoxEmailIterator implements Iterator {
+
+ private final Iterator mboxIterator;
+ private final CharsetEncoder encoder;
+ private final long fileID;
+ private final boolean wholeMsg;
+
+ MBoxEmailIterator(Iterator mboxIter, CharsetEncoder encoder, long fileID, boolean wholeMsg) {
+ mboxIterator = mboxIter;
+ this.encoder = encoder;
+ this.fileID = fileID;
+ this.wholeMsg = wholeMsg;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return (mboxIterator != null && encoder != null) ? mboxIterator.hasNext() : false;
+ }
+
+ @Override
+ public EmailMessage next() {
+ CharBufferWrapper messageBuffer = mboxIterator.next();
+
+ try {
+ Message msg = messageBuilder.parseMessage(messageBuffer.asInputStream(encoder.charset()));
+ if(wholeMsg) {
+ return extractEmail(msg, fileID);
+ } else {
+ return extractPartialEmail(msg);
+ }
+ } catch (RuntimeException | IOException ex) {
+ logger.log(Level.WARNING, "Failed to get message from mbox: {0}", ex.getMessage()); //NON-NLS
+ }
+ return null;
+ }
+
+ }
}
diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/PstParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/PstParser.java
index 704e3b2e14..32ab28352e 100644
--- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/PstParser.java
+++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/PstParser.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2011-2014 Basis Technology Corp.
+ * Copyright 2011-2019 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,6 +18,7 @@
*/
package org.sleuthkit.autopsy.thunderbirdparser;
+import com.google.common.collect.Iterables;
import com.pff.PSTAttachment;
import com.pff.PSTException;
import com.pff.PSTFile;
@@ -29,6 +30,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
@@ -55,112 +57,193 @@ class PstParser {
* First four bytes of a pst file.
*/
private static int PST_HEADER = 0x2142444E;
- private IngestServices services;
/**
* A map of PSTMessages to their Local path within the file's internal
* directory structure.
*/
- private List results;
- private StringBuilder errors;
-
+ private final StringBuilder errors;
+
+ private final IngestServices services;
+
+ private PSTFile pstFile;
+ private long fileID;
+
+ private int failureCount = 0;
+
PstParser(IngestServices services) {
- results = new ArrayList<>();
- this.services = services;
errors = new StringBuilder();
+ this.services = services;
}
enum ParseResult {
OK, ERROR, ENCRYPT;
}
-
/**
- * Parse and extract email messages from the pst/ost file.
- *
- * @param file A pst or ost file.
- *
- * @return ParseResult: OK on success, ERROR on an error, ENCRYPT if failed
- * because the file is encrypted.
+ * Create an instance of PSTFile for the given File object.
+ *
+ * The constructor for PSTFile object will throw a generic PSTException if the
+ * file is encrypted.
+ * PSTFile.java
+ *
+ * @param file File to open
+ * @param fileID File id for use when creating the EmailMessage objects
+ * @return ParserResult value OK if the PSTFile was successfully created,
+ * ENCRYPT will be returned for PSTExceptions that matches at specific
+ * message or IllegalArgumentExceptions
*/
- ParseResult parse(File file, long fileID) {
- PSTFile pstFile;
- long failures;
+ ParseResult open(File file, long fileID) {
+ if(file == null) {
+ return ParseResult.ERROR;
+ }
+
try {
pstFile = new PSTFile(file);
- failures = processFolder(pstFile.getRootFolder(), "\\", true, fileID);
- if (failures > 0) {
- addErrorMessage(
- NbBundle.getMessage(this.getClass(), "PstParser.parse.errMsg.failedToParseNMsgs", failures));
+ } catch(PSTException ex) {
+ // This is the message thrown from the PSTFile constructor if it
+ // detects that the file is encrypted.
+ if(ex.getMessage().equals("Only unencrypted and compressable PST files are supported at this time")) { //NON-NLS
+ logger.log(Level.INFO, "Found encrypted PST file."); //NON-NLS
+ return ParseResult.ENCRYPT;
}
- return ParseResult.OK;
- } catch (PSTException | IOException ex) {
String msg = file.getName() + ": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); //NON-NLS
- logger.log(Level.WARNING, msg);
+ logger.log(Level.WARNING, msg, ex);
return ParseResult.ERROR;
- } catch (IllegalArgumentException ex) {
+ } catch (IOException ex) {
+ String msg = file.getName() + ": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); //NON-NLS
+ logger.log(Level.WARNING, msg, ex);
+ return ParseResult.ERROR;
+ } catch (IllegalArgumentException ex) { // Not sure if this is true, was in previous version of code.
logger.log(Level.INFO, "Found encrypted PST file."); //NON-NLS
return ParseResult.ENCRYPT;
}
+
+ return ParseResult.OK;
}
-
+
/**
- * Get the results of the parsing.
- *
- * @return
+ * Creates an EmailMessage iterator for pstFile. These Email objects will be
+ * complete and with all available information.
+ *
+ * @return A instance of an EmailMessage Iterator
*/
- List getResults() {
- return results;
+ Iterator getEmailMessageIterator() {
+ if(pstFile == null) {
+ return null;
+ }
+
+ Iterable iterable = null;
+
+ try {
+ iterable = getEmaiMessageIterator(pstFile.getRootFolder(), "\\", fileID, true);
+ } catch (PSTException | IOException ex) {
+ logger.log(Level.WARNING, String.format("Exception thrown while parsing fileID: %d", fileID), ex);
+ }
+
+ if(iterable == null) {
+ return null;
+ }
+
+ return iterable.iterator();
}
-
+
+ /**
+ * Get a List of EmailMessages which contain only the information needed for
+ * threading the emails.
+ *
+ * @return A list of EmailMessage or an empty list if non were found.
+ */
+ List getPartialEmailMessages() {
+ List messages = new ArrayList<>();
+ Iterator iterator = getPartialEmailMessageIterator();
+ if(iterator != null) {
+ while(iterator.hasNext()) {
+ messages.add(iterator.next());
+ }
+ }
+
+ return messages;
+ }
+
+ /**
+ *
+ * @return
+ */
String getErrors() {
return errors.toString();
}
-
- /**
- * Process this folder and all subfolders, adding every email found to
- * results. Accumulates the folder hierarchy path as it navigates the folder
- * structure.
- *
- * @param folder The folder to navigate and process
- * @param path The path to the folder within the pst/ost file's directory
- * structure
- *
- * @throws PSTException
- * @throws IOException
+
+ int getFailureCount() {
+ return failureCount;
+ }
+
+ /**
+ * Get an Iterator to which will iterate over the PSTFile, but return EmailMessages
+ * with only the information needed for putting the emails into threads.
+ *
+ * @return A EmailMessage iterator or null if no messages where found
*/
- private long processFolder(PSTFolder folder, String path, boolean root, long fileID) {
- String newPath = (root ? path : path + "\\" + folder.getDisplayName());
- long failCount = 0L; // Number of emails that failed
- if (folder.hasSubfolders()) {
- List subFolders;
- try {
- subFolders = folder.getSubFolders();
- } catch (PSTException | IOException ex) {
- subFolders = new ArrayList<>();
- logger.log(Level.INFO, "java-libpst exception while getting subfolders: {0}", ex.getMessage()); //NON-NLS
- }
-
- for (PSTFolder f : subFolders) {
- failCount += processFolder(f, newPath, false, fileID);
- }
+ private Iterator getPartialEmailMessageIterator() {
+ if(pstFile == null) {
+ return null;
}
-
- if (folder.getContentCount() != 0) {
- PSTMessage email;
- // A folder's children are always emails, never other folders.
- try {
- while ((email = (PSTMessage) folder.getNextChild()) != null) {
- results.add(extractEmailMessage(email, newPath, fileID));
- }
- } catch (PSTException | IOException ex) {
- failCount++;
- logger.log(Level.INFO, "java-libpst exception while getting emails from a folder: {0}", ex.getMessage()); //NON-NLS
- }
+
+ Iterable iterable = null;
+
+ try {
+ iterable = getEmaiMessageIterator(pstFile.getRootFolder(), "\\", fileID, false);
+ } catch (PSTException | IOException ex) {
+ logger.log(Level.WARNING, String.format("Exception thrown while parsing fileID: %d", fileID), ex);
}
-
- return failCount;
+
+ if(iterable == null) {
+ return null;
+ }
+
+ return iterable.iterator();
}
+ /**
+ * Creates an Iterable object of Email messages for the given folder.
+ *
+ * @param folder PSTFolder to process
+ * @param path String path to folder
+ * @param fileID FileID of the AbstractFile folder was found in
+ * @param partialEmail Whether or not fill the EMailMessage with all data
+ *
+ * @return An Iterable for iterating email message, or null if there were no
+ * messages or children in folder.
+ *
+ * @throws PSTException
+ * @throws IOException
+ */
+ private Iterable getEmaiMessageIterator(PSTFolder folder, String path, long fileID, boolean wholeMsg) throws PSTException, IOException {
+ Iterable iterable = null;
+
+ if(folder.getContentCount() > 0) {
+ iterable = new PstEmailIterator(folder, path, fileID, wholeMsg).getIterable();
+ }
+
+ if(folder.hasSubfolders()) {
+ List subFolders = folder.getSubFolders();
+ for(PSTFolder subFolder: subFolders) {
+ String newpath = path + "\\" + subFolder.getDisplayName();
+ Iterable subIterable = getEmaiMessageIterator(subFolder, newpath, fileID, wholeMsg);
+ if(subIterable == null) {
+ continue;
+ }
+
+ if(iterable != null) {
+ iterable = Iterables.concat(iterable, subIterable);
+ } else {
+ iterable = subIterable;
+ }
+
+ }
+ }
+
+ return iterable;
+ }
/**
* Create an EmailMessage from a PSTMessage.
*
@@ -214,6 +297,33 @@ class PstParser {
return email;
}
+
+ /**
+ * Create an EmailMessage from a PSTMessage with only the information
+ * needed for threading emails.
+ *
+ * @return EmailMessage object with only some information, not all of the msg.
+ */
+ private EmailMessage extractPartialEmailMessage(PSTMessage msg) {
+ EmailMessage email = new EmailMessage();
+ email.setSubject(msg.getSubject());
+ email.setId(msg.getDescriptorNodeId());
+ email.setMessageID(msg.getInternetMessageId());
+ String inReplyToID = msg.getInReplyToId();
+ email.setInReplyToID(inReplyToID);
+ List references = extractReferences(msg.getTransportMessageHeaders());
+ if (inReplyToID != null && !inReplyToID.isEmpty()) {
+ if (references == null) {
+ references = new ArrayList<>();
+ references.add(inReplyToID);
+ } else if (!references.contains(inReplyToID)) {
+ references.add(inReplyToID);
+ }
+ }
+ email.setReferences(references);
+
+ return email;
+ }
/**
* Add the attachments within the PSTMessage to the EmailMessage.
@@ -388,5 +498,95 @@ class PstParser {
}
return null;
+ }
+
+ /**
+ * A iterator for processing the PST email folder structure and returning
+ * instances of the EmailMessage object.
+ */
+ private final class PstEmailIterator implements Iterator {
+
+ private final PSTFolder folder;
+ private EmailMessage current;
+ private EmailMessage next;
+
+ private final String currentPath;
+ private final long fileID;
+ private final boolean wholeMsg;
+
+ /**
+ * Class constructor, initializes the "next" message;
+ *
+ * @param folder PSTFolder object to iterate across
+ * @param path String path value to the location of folder
+ * @param fileID Long fileID of the abstract file this PSTFolder was found
+ */
+ PstEmailIterator(PSTFolder folder, String path, long fileID, boolean wholeMsg) {
+ this.folder = folder;
+ this.fileID = fileID;
+ this.currentPath = path;
+ this.wholeMsg = wholeMsg;
+
+ if(folder.getContentCount() > 0) {
+ try {
+ PSTMessage message = (PSTMessage)folder.getNextChild();
+ if(message != null) {
+ if(wholeMsg) {
+ next = extractEmailMessage(message, currentPath, fileID);
+ } else {
+ next = extractPartialEmailMessage(message);
+ }
+ }
+ } catch (PSTException | IOException ex) {
+ failureCount++;
+ logger.log(Level.WARNING, String.format("Unable to extract emails for path: %s file ID: %d ", path, fileID), ex);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ @Override
+ public EmailMessage next() {
+
+ current = next;
+
+ try {
+ PSTMessage message = (PSTMessage)folder.getNextChild();
+ if(message != null) {
+ if(wholeMsg) {
+ next = extractEmailMessage(message, currentPath, fileID);
+ } else {
+ next = extractPartialEmailMessage(message);
+ }
+ } else {
+ next = null;
+ }
+ } catch (PSTException | IOException ex) {
+ logger.log(Level.WARNING, String.format("Unable to extract emails for path: %s file ID: %d ", currentPath, fileID), ex);
+ failureCount++;
+ next = null;
+ }
+
+ return current;
+ }
+
+ /**
+ * Get a wrapped Iterable version of PstEmailIterator
+ *
+ * @return Iterable wrapping this class
+ */
+ Iterable getIterable(){
+ return new Iterable(){
+ @Override
+ public Iterator iterator() {
+ return PstEmailIterator.this;
+ }
+ };
+ }
+
}
}
diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java
index 33da281def..b413400404 100644
--- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java
+++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
@@ -183,51 +184,53 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
}
PstParser parser = new PstParser(services);
- PstParser.ParseResult result = parser.parse(file, abstractFile.getId());
+ PstParser.ParseResult result = parser.open(file, abstractFile.getId());
- if (result == PstParser.ParseResult.OK) {
- // parse success: Process email and add artifacts
- processEmails(parser.getResults(), abstractFile);
-
- } else if (result == PstParser.ParseResult.ENCRYPT) {
- // encrypted pst: Add encrypted file artifact
- try {
- BlackboardArtifact artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED);
- artifact.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME, EmailParserModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.encryptionFileLevel")));
+ switch( result) {
+ case OK:
+ processEmails(parser.getPartialEmailMessages(), parser.getEmailMessageIterator(), abstractFile);
+ break;
+ case ENCRYPT:
+ // encrypted pst: Add encrypted file artifact
try {
- // index the artifact for keyword search
- blackboard.indexArtifact(artifact);
- } catch (Blackboard.BlackboardException ex) {
- MessageNotifyUtil.Notify.error(Bundle.ThunderbirdMboxFileIngestModule_processPst_indexError_message(), artifact.getDisplayName());
- logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
- }
+ BlackboardArtifact artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED);
+ artifact.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME, EmailParserModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.encryptionFileLevel")));
- services.fireModuleDataEvent(new ModuleDataEvent(EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED));
- } catch (TskCoreException ex) {
- logger.log(Level.INFO, "Failed to add encryption attribute to file: {0}", abstractFile.getName()); //NON-NLS
- }
- } else {
- // parsing error: log message
- postErrorMessage(
- NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.processPst.errProcFile.msg",
- abstractFile.getName()),
- NbBundle.getMessage(this.getClass(),
- "ThunderbirdMboxFileIngestModule.processPst.errProcFile.details"));
- logger.log(Level.INFO, "PSTParser failed to parse {0}", abstractFile.getName()); //NON-NLS
- return ProcessResult.ERROR;
+ try {
+ // index the artifact for keyword search
+ blackboard.indexArtifact(artifact);
+ } catch (Blackboard.BlackboardException ex) {
+ MessageNotifyUtil.Notify.error(Bundle.ThunderbirdMboxFileIngestModule_processPst_indexError_message(), artifact.getDisplayName());
+ logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
+ }
+
+ services.fireModuleDataEvent(new ModuleDataEvent(EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED));
+ } catch (TskCoreException ex) {
+ logger.log(Level.INFO, "Failed to add encryption attribute to file: {0}", abstractFile.getName()); //NON-NLS
+ }
+ break;
+ default:
+ // parsing error: log message
+ postErrorMessage(
+ NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.processPst.errProcFile.msg",
+ abstractFile.getName()),
+ NbBundle.getMessage(this.getClass(),
+ "ThunderbirdMboxFileIngestModule.processPst.errProcFile.details"));
+ logger.log(Level.INFO, "PSTParser failed to parse {0}", abstractFile.getName()); //NON-NLS
+ return ProcessResult.ERROR;
}
if (file.delete() == false) {
logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS
}
- String errors = parser.getErrors();
- if (errors.isEmpty() == false) {
- postErrorMessage(
- NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.processPst.errProcFile.msg2",
- abstractFile.getName()), errors);
- }
+// String errors = parser.getErrors();
+// if (errors.isEmpty() == false) {
+// postErrorMessage(
+// NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.processPst.errProcFile.msg2",
+// abstractFile.getName()), errors);
+// }
return ProcessResult.OK;
}
@@ -281,21 +284,29 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
return ProcessResult.OK;
}
- MboxParser parser = new MboxParser(services, emailFolder);
- List emails = parser.parse(file, abstractFile.getId());
- processEmails(emails, abstractFile);
+ MboxParser emailIterator = MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId());
+ List emails = new ArrayList<>();
+ if(emailIterator != null) {
+ while(emailIterator.hasNext()) {
+ EmailMessage emailMessage = emailIterator.next();
+ if(emailMessage != null) {
+ emails.add(emailMessage);
+ }
+ }
+
+ String errors = emailIterator.getErrors();
+ if (!errors.isEmpty()) {
+ postErrorMessage(
+ NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.processMBox.errProcFile.msg2",
+ abstractFile.getName()), errors);
+ }
+ }
+ processEmails(emails, MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()), abstractFile);
if (file.delete() == false) {
logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS
}
- String errors = parser.getErrors();
- if (errors.isEmpty() == false) {
- postErrorMessage(
- NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.processMBox.errProcFile.msg2",
- abstractFile.getName()), errors);
- }
-
return ProcessResult.OK;
}
@@ -434,7 +445,50 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
context.addFilesToJob(derivedFiles);
services.fireModuleDataEvent(new ModuleDataEvent(EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG));
}
+
+ private void processEmails(List partialEmailsForThreading, Iterator fullMessageIterator, AbstractFile abstractFile) {
+ // Putting try/catch around this to catch any exception and still allow
+ // the creation of the artifacts to continue.
+ try{
+ EmailMessageThreader.threadMessages(partialEmailsForThreading, String.format("%d", abstractFile.getId()));
+ } catch(Exception ex) {
+ logger.log(Level.WARNING, String.format("Exception thrown parsing emails from %s", abstractFile.getName()), ex);
+ }
+
+ List derivedFiles = new ArrayList<>();
+ int msgCnt = 0;
+ while(fullMessageIterator.hasNext()) {
+ EmailMessage current = fullMessageIterator.next();
+
+ if(current == null) {
+ continue;
+ }
+
+ if(partialEmailsForThreading.size() > msgCnt) {
+ EmailMessage threaded = partialEmailsForThreading.get(msgCnt++);
+
+ if(threaded.getMessageID().equals(current.getMessageID()) &&
+ threaded.getSubject().equals(current.getSubject())) {
+ current.setMessageThreadID(threaded.getMessageThreadID());
+ }
+ }
+
+ BlackboardArtifact msgArtifact = addEmailArtifact(current, abstractFile);
+
+ if ((msgArtifact != null) && (current.hasAttachment())) {
+ derivedFiles.addAll(handleAttachments(current.getAttachments(), abstractFile, msgArtifact ));
+ }
+ }
+
+ if (derivedFiles.isEmpty() == false) {
+ for (AbstractFile derived : derivedFiles) {
+ services.fireModuleContentEvent(new ModuleContentEvent(derived));
+ }
+ }
+ context.addFilesToJob(derivedFiles);
+ services.fireModuleDataEvent(new ModuleDataEvent(EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG));
+ }
/**
* Add the given attachments as derived files and reschedule them for
* ingest.