Autopsy  4.17.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
PstParser.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2019 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.thunderbirdparser;
20 
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;
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.RandomAccessFile;
32 import java.nio.ByteBuffer;
33 import java.util.ArrayList;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Scanner;
37 import java.util.logging.Level;
39 import org.openide.util.NbBundle;
44 import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
45 import org.sleuthkit.datamodel.AbstractFile;
46 import org.sleuthkit.datamodel.EncodedFileOutputStream;
47 import org.sleuthkit.datamodel.TskCoreException;
48 import org.sleuthkit.datamodel.TskData;
49 
55 class PstParser implements AutoCloseable{
56 
57  private static final Logger logger = Logger.getLogger(PstParser.class.getName());
61  private static int PST_HEADER = 0x2142444E;
62 
63  private final IngestServices services;
64 
65  private PSTFile pstFile;
66  private long fileID;
67 
68  private int failureCount = 0;
69 
70  private final List<String> errorList = new ArrayList<>();
71 
72  PstParser(IngestServices services) {
73  this.services = services;
74  }
75 
76  enum ParseResult {
77 
78  OK, ERROR, ENCRYPT;
79  }
80 
96  ParseResult open(File file, long fileID) {
97  if (file == null) {
98  return ParseResult.ERROR;
99  }
100 
101  try {
102  pstFile = new PSTFile(file);
103  } catch (PSTException ex) {
104  // This is the message thrown from the PSTFile constructor if it
105  // detects that the file is encrypted.
106  if (ex.getMessage().equals("Only unencrypted and compressable PST files are supported at this time")) { //NON-NLS
107  logger.log(Level.INFO, "Found encrypted PST file."); //NON-NLS
108  return ParseResult.ENCRYPT;
109  }
110  String msg = file.getName() + ": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); //NON-NLS
111  logger.log(Level.WARNING, msg, ex);
112  return ParseResult.ERROR;
113  } catch (IOException ex) {
114  String msg = file.getName() + ": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); //NON-NLS
115  logger.log(Level.WARNING, msg, ex);
116  return ParseResult.ERROR;
117  } catch (IllegalArgumentException ex) { // Not sure if this is true, was in previous version of code.
118  logger.log(Level.INFO, "Found encrypted PST file."); //NON-NLS
119  return ParseResult.ENCRYPT;
120  }
121 
122  return ParseResult.OK;
123  }
124 
125  @Override
126  public void close() throws Exception{
127  if(pstFile != null) {
128  RandomAccessFile file = pstFile.getFileHandle();
129  if(file != null) {
130  file.close();
131  }
132  }
133  }
134 
141  Iterator<EmailMessage> getEmailMessageIterator() {
142  if (pstFile == null) {
143  return null;
144  }
145 
146  Iterable<EmailMessage> iterable = null;
147 
148  try {
149  iterable = getEmailMessageIterator(pstFile.getRootFolder(), "\\", fileID, true);
150  } catch (PSTException | IOException ex) {
151  logger.log(Level.WARNING, String.format("Exception thrown while parsing fileID: %d", fileID), ex);
152  }
153 
154  if (iterable == null) {
155  return null;
156  }
157 
158  return iterable.iterator();
159  }
160 
167  List<EmailMessage> getPartialEmailMessages() {
168  List<EmailMessage> messages = new ArrayList<>();
169  Iterator<EmailMessage> iterator = getPartialEmailMessageIterator();
170  if (iterator != null) {
171  while (iterator.hasNext()) {
172  messages.add(iterator.next());
173  }
174  }
175 
176  return messages;
177  }
178 
184  String getErrors() {
185  String result = "";
186  for (String msg: errorList) {
187  result += "<li>" + msg + "</li>";
188  }
189  return result;
190  }
191 
197  int getFailureCount() {
198  return failureCount;
199  }
200 
208  private Iterator<EmailMessage> getPartialEmailMessageIterator() {
209  if (pstFile == null) {
210  return null;
211  }
212 
213  Iterable<EmailMessage> iterable = null;
214 
215  try {
216  iterable = getEmailMessageIterator(pstFile.getRootFolder(), "\\", fileID, false);
217  } catch (PSTException | IOException ex) {
218  logger.log(Level.WARNING, String.format("Exception thrown while parsing fileID: %d", fileID), ex);
219  }
220 
221  if (iterable == null) {
222  return null;
223  }
224 
225  return iterable.iterator();
226  }
227 
242  private Iterable<EmailMessage> getEmailMessageIterator(PSTFolder folder, String path, long fileID, boolean wholeMsg) throws PSTException, IOException {
243  Iterable<EmailMessage> iterable = null;
244 
245  if (folder.getContentCount() > 0) {
246  iterable = new PstEmailIterator(folder, path, fileID, wholeMsg).getIterable();
247  }
248 
249  if (folder.hasSubfolders()) {
250  List<PSTFolder> subFolders = folder.getSubFolders();
251  for (PSTFolder subFolder : subFolders) {
252  String newpath = path + "\\" + subFolder.getDisplayName();
253  Iterable<EmailMessage> subIterable = getEmailMessageIterator(subFolder, newpath, fileID, wholeMsg);
254  if (subIterable == null) {
255  continue;
256  }
257 
258  if (iterable != null) {
259  iterable = Iterables.concat(iterable, subIterable);
260  } else {
261  iterable = subIterable;
262  }
263 
264  }
265  }
266 
267  return iterable;
268  }
269 
278  private EmailMessage extractEmailMessage(PSTMessage msg, String localPath, long fileID) {
279  EmailMessage email = new EmailMessage();
280  email.setRecipients(msg.getDisplayTo());
281  email.setCc(msg.getDisplayCC());
282  email.setBcc(msg.getDisplayBCC());
283  email.setSender(getSender(msg.getSenderName(), msg.getSenderEmailAddress()));
284  email.setSentDate(msg.getMessageDeliveryTime());
285  email.setTextBody(msg.getBody());
286  if (false == msg.getTransportMessageHeaders().isEmpty()) {
287  email.setHeaders("\n-----HEADERS-----\n\n" + msg.getTransportMessageHeaders() + "\n\n---END HEADERS--\n\n");
288  }
289 
290  email.setHtmlBody(msg.getBodyHTML());
291  String rtf = "";
292  try {
293  rtf = msg.getRTFBody();
294  } catch (PSTException | IOException ex) {
295  logger.log(Level.INFO, "Failed to get RTF content from pst email."); //NON-NLS
296  }
297  email.setRtfBody(rtf);
298  email.setLocalPath(localPath);
299  email.setSubject(msg.getSubject());
300  email.setId(msg.getDescriptorNodeId());
301  email.setMessageID(msg.getInternetMessageId());
302 
303  String inReplyToID = msg.getInReplyToId();
304  email.setInReplyToID(inReplyToID);
305 
306  if (msg.hasAttachments()) {
307  extractAttachments(email, msg, fileID);
308  }
309 
310  List<String> references = extractReferences(msg.getTransportMessageHeaders());
311  if (inReplyToID != null && !inReplyToID.isEmpty()) {
312  if (references == null) {
313  references = new ArrayList<>();
314  references.add(inReplyToID);
315  } else if (!references.contains(inReplyToID)) {
316  references.add(inReplyToID);
317  }
318  }
319  email.setReferences(references);
320 
321  return email;
322  }
323 
331  private EmailMessage extractPartialEmailMessage(PSTMessage msg) {
332  EmailMessage email = new EmailMessage();
333  email.setSubject(msg.getSubject());
334  email.setId(msg.getDescriptorNodeId());
335  email.setMessageID(msg.getInternetMessageId());
336  String inReplyToID = msg.getInReplyToId();
337  email.setInReplyToID(inReplyToID);
338  List<String> references = extractReferences(msg.getTransportMessageHeaders());
339  if (inReplyToID != null && !inReplyToID.isEmpty()) {
340  if (references == null) {
341  references = new ArrayList<>();
342  references.add(inReplyToID);
343  } else if (!references.contains(inReplyToID)) {
344  references.add(inReplyToID);
345  }
346  }
347  email.setReferences(references);
348 
349  return email;
350  }
351 
358  @NbBundle.Messages({"PstParser.noOpenCase.errMsg=Exception while getting open case."})
359  private void extractAttachments(EmailMessage email, PSTMessage msg, long fileID) {
360  int numberOfAttachments = msg.getNumberOfAttachments();
361  String outputDirPath;
362  try {
363  outputDirPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator;
364  } catch (NoCurrentCaseException ex) {
365  logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
366  return;
367  }
368  for (int x = 0; x < numberOfAttachments; x++) {
369  String filename = "";
370  try {
371  PSTAttachment attach = msg.getAttachment(x);
372  long size = attach.getAttachSize();
373  long freeSpace = services.getFreeDiskSpace();
374  if ((freeSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && (size >= freeSpace)) {
375  continue;
376  }
377  // both long and short filenames can be used for attachments
378  filename = attach.getLongFilename();
379  if (filename.isEmpty()) {
380  filename = attach.getFilename();
381  }
382  String uniqueFilename = fileID + "-" + msg.getDescriptorNodeId() + "-" + attach.getContentId() + "-" + FileUtil.escapeFileName(filename);
383  String outPath = outputDirPath + uniqueFilename;
384  saveAttachmentToDisk(attach, outPath);
385 
386  EmailMessage.Attachment attachment = new EmailMessage.Attachment();
387 
388  long crTime = attach.getCreationTime() != null ? attach.getCreationTime().getTime() / 1000 : 0;
389  long mTime = attach.getModificationTime() != null ? attach.getModificationTime().getTime() / 1000 : 0;
390  String relPath = getRelModuleOutputPath() + File.separator + uniqueFilename;
391  attachment.setName(filename);
392  attachment.setCrTime(crTime);
393  attachment.setmTime(mTime);
394  attachment.setLocalPath(relPath);
395  attachment.setSize(attach.getFilesize());
396  attachment.setEncodingType(TskData.EncodingType.XOR1);
397  email.addAttachment(attachment);
398  } catch (PSTException | IOException | NullPointerException ex) {
403  addErrorMessage(
404  NbBundle.getMessage(this.getClass(), "PstParser.extractAttch.errMsg.failedToExtractToDisk",
405  filename));
406  logger.log(Level.WARNING, "Failed to extract attachment from pst file.", ex); //NON-NLS
407  } catch (NoCurrentCaseException ex) {
408  addErrorMessage(Bundle.PstParser_noOpenCase_errMsg());
409  logger.log(Level.SEVERE, Bundle.PstParser_noOpenCase_errMsg(), ex); //NON-NLS
410  }
411  }
412  }
413 
423  private void saveAttachmentToDisk(PSTAttachment attach, String outPath) throws IOException, PSTException {
424  try (InputStream attachmentStream = attach.getFileInputStream();
425  EncodedFileOutputStream out = new EncodedFileOutputStream(new FileOutputStream(outPath), TskData.EncodingType.XOR1)) {
426  // 8176 is the block size used internally and should give the best performance
427  int bufferSize = 8176;
428  byte[] buffer = new byte[bufferSize];
429  int count = attachmentStream.read(buffer);
430 
431  if (count == -1) {
432  throw new IOException("attachmentStream invalid (read() fails). File " + attach.getLongFilename() + " skipped");
433  }
434 
435  while (count == bufferSize) {
436  out.write(buffer);
437  count = attachmentStream.read(buffer);
438  }
439  if (count != -1) {
440  byte[] endBuffer = new byte[count];
441  System.arraycopy(buffer, 0, endBuffer, 0, count);
442  out.write(endBuffer);
443  }
444  }
445  }
446 
455  private String getSender(String name, String addr) {
456  if (name.isEmpty() && addr.isEmpty()) {
457  return "";
458  } else if (name.isEmpty()) {
459  return addr;
460  } else if (addr.isEmpty()) {
461  return name;
462  } else {
463  return name + ": " + addr;
464  }
465  }
466 
474  public static boolean isPstFile(AbstractFile file) {
475  byte[] buffer = new byte[4];
476  try {
477  int read = file.read(buffer, 0, 4);
478  if (read != 4) {
479  return false;
480  }
481  ByteBuffer bb = ByteBuffer.wrap(buffer);
482  return bb.getInt() == PST_HEADER;
483  } catch (TskCoreException ex) {
484  logger.log(Level.WARNING, "Exception while detecting if a file is a pst file."); //NON-NLS
485  return false;
486  }
487  }
488 
494  private void addErrorMessage(String msg) {
495  errorList.add(msg);
496  }
497 
505  private List<String> extractReferences(String emailHeader) {
506  Scanner scanner = new Scanner(emailHeader);
507  StringBuilder buffer = null;
508  while (scanner.hasNextLine()) {
509  String token = scanner.nextLine();
510 
511  if (token.matches("^References:.*")) {
512  buffer = new StringBuilder();
513  buffer.append((token.substring(token.indexOf(':') + 1)).trim());
514  } else if (buffer != null) {
515  if (token.matches("^\\w+:.*$")) {
516  List<String> references = new ArrayList<>();
517  for (String id : buffer.toString().split(">")) {
518  references.add(id.trim() + ">");
519  }
520  return references;
521  } else {
522  buffer.append(token.trim());
523  }
524  }
525  }
526 
527  return null;
528  }
529 
534  private final class PstEmailIterator implements Iterator<EmailMessage> {
535 
536  private final PSTFolder folder;
537  private EmailMessage currentMsg;
538  private EmailMessage nextMsg;
539 
540  private final String currentPath;
541  private final long fileID;
542  private final boolean wholeMsg;
543 
552  PstEmailIterator(PSTFolder folder, String path, long fileID, boolean wholeMsg) {
553  this.folder = folder;
554  this.fileID = fileID;
555  this.currentPath = path;
556  this.wholeMsg = wholeMsg;
557 
558  if (folder.getContentCount() > 0) {
559  try {
560  PSTMessage message = (PSTMessage) folder.getNextChild();
561  if (message != null) {
562  if (wholeMsg) {
563  nextMsg = extractEmailMessage(message, currentPath, fileID);
564  } else {
565  nextMsg = extractPartialEmailMessage(message);
566  }
567  }
568  } catch (PSTException | IOException ex) {
569  failureCount++;
570  logger.log(Level.WARNING, String.format("Unable to extract emails for path: %s file ID: %d ", path, fileID), ex);
571  }
572  }
573  }
574 
575  @Override
576  public boolean hasNext() {
577  return nextMsg != null;
578  }
579 
580  @Override
581  public EmailMessage next() {
582 
583  currentMsg = nextMsg;
584 
585  try {
586  PSTMessage message = (PSTMessage) folder.getNextChild();
587  if (message != null) {
588  if (wholeMsg) {
589  nextMsg = extractEmailMessage(message, currentPath, fileID);
590  } else {
591  nextMsg = extractPartialEmailMessage(message);
592  }
593  } else {
594  nextMsg = null;
595  }
596  } catch (PSTException | IOException ex) {
597  logger.log(Level.WARNING, String.format("Unable to extract emails for path: %s file ID: %d ", currentPath, fileID), ex);
598  failureCount++;
599  nextMsg = null;
600  }
601 
602  return currentMsg;
603  }
604 
610  Iterable<EmailMessage> getIterable() {
611  return new Iterable<EmailMessage>() {
612  @Override
613  public Iterator<EmailMessage> iterator() {
614  return PstEmailIterator.this;
615  }
616  };
617  }
618 
619  }
620 }

Copyright © 2012-2021 Basis Technology. Generated on: Tue Jan 19 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.