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 = getEmailMessageIterator(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 = getEmailMessageIterator(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> getEmailMessageIterator(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 = getEmailMessageIterator(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() {