Autopsy  4.16.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.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;
43 import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
44 import org.sleuthkit.datamodel.AbstractFile;
45 import org.sleuthkit.datamodel.EncodedFileOutputStream;
46 import org.sleuthkit.datamodel.TskCoreException;
47 import org.sleuthkit.datamodel.TskData;
48 
54 class PstParser {
55 
56  private static final Logger logger = Logger.getLogger(PstParser.class.getName());
60  private static int PST_HEADER = 0x2142444E;
61 
62  private final IngestServices services;
63 
64  private PSTFile pstFile;
65  private long fileID;
66 
67  private int failureCount = 0;
68 
69  private final List<String> errorList = new ArrayList<>();
70 
71  PstParser(IngestServices services) {
72  this.services = services;
73  }
74 
75  enum ParseResult {
76 
77  OK, ERROR, ENCRYPT;
78  }
79 
95  ParseResult open(File file, long fileID) {
96  if (file == null) {
97  return ParseResult.ERROR;
98  }
99 
100  try {
101  pstFile = new PSTFile(file);
102  } catch (PSTException ex) {
103  // This is the message thrown from the PSTFile constructor if it
104  // detects that the file is encrypted.
105  if (ex.getMessage().equals("Only unencrypted and compressable PST files are supported at this time")) { //NON-NLS
106  logger.log(Level.INFO, "Found encrypted PST file."); //NON-NLS
107  return ParseResult.ENCRYPT;
108  }
109  String msg = file.getName() + ": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage(); //NON-NLS
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(); //NON-NLS
114  logger.log(Level.WARNING, msg, ex);
115  return ParseResult.ERROR;
116  } catch (IllegalArgumentException ex) { // Not sure if this is true, was in previous version of code.
117  logger.log(Level.INFO, "Found encrypted PST file."); //NON-NLS
118  return ParseResult.ENCRYPT;
119  }
120 
121  return ParseResult.OK;
122  }
123 
130  Iterator<EmailMessage> getEmailMessageIterator() {
131  if (pstFile == null) {
132  return null;
133  }
134 
135  Iterable<EmailMessage> iterable = null;
136 
137  try {
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);
141  }
142 
143  if (iterable == null) {
144  return null;
145  }
146 
147  return iterable.iterator();
148  }
149 
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());
162  }
163  }
164 
165  return messages;
166  }
167 
173  String getErrors() {
174  String result = "";
175  for (String msg: errorList) {
176  result += "<li>" + msg + "</li>";
177  }
178  return result;
179  }
180 
186  int getFailureCount() {
187  return failureCount;
188  }
189 
197  private Iterator<EmailMessage> getPartialEmailMessageIterator() {
198  if (pstFile == null) {
199  return null;
200  }
201 
202  Iterable<EmailMessage> iterable = null;
203 
204  try {
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);
208  }
209 
210  if (iterable == null) {
211  return null;
212  }
213 
214  return iterable.iterator();
215  }
216 
231  private Iterable<EmailMessage> getEmailMessageIterator(PSTFolder folder, String path, long fileID, boolean wholeMsg) throws PSTException, IOException {
232  Iterable<EmailMessage> iterable = null;
233 
234  if (folder.getContentCount() > 0) {
235  iterable = new PstEmailIterator(folder, path, fileID, wholeMsg).getIterable();
236  }
237 
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) {
244  continue;
245  }
246 
247  if (iterable != null) {
248  iterable = Iterables.concat(iterable, subIterable);
249  } else {
250  iterable = subIterable;
251  }
252 
253  }
254  }
255 
256  return iterable;
257  }
258 
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");
277  }
278 
279  email.setHtmlBody(msg.getBodyHTML());
280  String rtf = "";
281  try {
282  rtf = msg.getRTFBody();
283  } catch (PSTException | IOException ex) {
284  logger.log(Level.INFO, "Failed to get RTF content from pst email."); //NON-NLS
285  }
286  email.setRtfBody(rtf);
287  email.setLocalPath(localPath);
288  email.setSubject(msg.getSubject());
289  email.setId(msg.getDescriptorNodeId());
290  email.setMessageID(msg.getInternetMessageId());
291 
292  String inReplyToID = msg.getInReplyToId();
293  email.setInReplyToID(inReplyToID);
294 
295  if (msg.hasAttachments()) {
296  extractAttachments(email, msg, fileID);
297  }
298 
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);
306  }
307  }
308  email.setReferences(references);
309 
310  return email;
311  }
312 
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);
334  }
335  }
336  email.setReferences(references);
337 
338  return email;
339  }
340 
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;
351  try {
352  outputDirPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator;
353  } catch (NoCurrentCaseException ex) {
354  logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
355  return;
356  }
357  for (int x = 0; x < numberOfAttachments; x++) {
358  String filename = "";
359  try {
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)) {
364  continue;
365  }
366  // both long and short filenames can be used for attachments
367  filename = attach.getLongFilename();
368  if (filename.isEmpty()) {
369  filename = attach.getFilename();
370  }
371  String uniqueFilename = fileID + "-" + msg.getDescriptorNodeId() + "-" + attach.getContentId() + "-" + FileUtil.escapeFileName(filename);
372  String outPath = outputDirPath + uniqueFilename;
373  saveAttachmentToDisk(attach, outPath);
374 
375  EmailMessage.Attachment attachment = new EmailMessage.Attachment();
376 
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) {
392  addErrorMessage(
393  NbBundle.getMessage(this.getClass(), "PstParser.extractAttch.errMsg.failedToExtractToDisk",
394  filename));
395  logger.log(Level.WARNING, "Failed to extract attachment from pst file.", ex); //NON-NLS
396  } catch (NoCurrentCaseException ex) {
397  addErrorMessage(Bundle.PstParser_noOpenCase_errMsg());
398  logger.log(Level.SEVERE, Bundle.PstParser_noOpenCase_errMsg(), ex); //NON-NLS
399  }
400  }
401  }
402 
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)) {
415  // 8176 is the block size used internally and should give the best performance
416  int bufferSize = 8176;
417  byte[] buffer = new byte[bufferSize];
418  int count = attachmentStream.read(buffer);
419 
420  if (count == -1) {
421  throw new IOException("attachmentStream invalid (read() fails). File " + attach.getLongFilename() + " skipped");
422  }
423 
424  while (count == bufferSize) {
425  out.write(buffer);
426  count = attachmentStream.read(buffer);
427  }
428  if (count != -1) {
429  byte[] endBuffer = new byte[count];
430  System.arraycopy(buffer, 0, endBuffer, 0, count);
431  out.write(endBuffer);
432  }
433  }
434  }
435 
444  private String getSender(String name, String addr) {
445  if (name.isEmpty() && addr.isEmpty()) {
446  return "";
447  } else if (name.isEmpty()) {
448  return addr;
449  } else if (addr.isEmpty()) {
450  return name;
451  } else {
452  return name + ": " + addr;
453  }
454  }
455 
463  public static boolean isPstFile(AbstractFile file) {
464  byte[] buffer = new byte[4];
465  try {
466  int read = file.read(buffer, 0, 4);
467  if (read != 4) {
468  return false;
469  }
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."); //NON-NLS
474  return false;
475  }
476  }
477 
483  private void addErrorMessage(String msg) {
484  errorList.add(msg);
485  }
486 
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();
499 
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() + ">");
508  }
509  return references;
510  } else {
511  buffer.append(token.trim());
512  }
513  }
514  }
515 
516  return null;
517  }
518 
523  private final class PstEmailIterator implements Iterator<EmailMessage> {
524 
525  private final PSTFolder folder;
526  private EmailMessage currentMsg;
527  private EmailMessage nextMsg;
528 
529  private final String currentPath;
530  private final long fileID;
531  private final boolean wholeMsg;
532 
541  PstEmailIterator(PSTFolder folder, String path, long fileID, boolean wholeMsg) {
542  this.folder = folder;
543  this.fileID = fileID;
544  this.currentPath = path;
545  this.wholeMsg = wholeMsg;
546 
547  if (folder.getContentCount() > 0) {
548  try {
549  PSTMessage message = (PSTMessage) folder.getNextChild();
550  if (message != null) {
551  if (wholeMsg) {
552  nextMsg = extractEmailMessage(message, currentPath, fileID);
553  } else {
554  nextMsg = extractPartialEmailMessage(message);
555  }
556  }
557  } catch (PSTException | IOException ex) {
558  failureCount++;
559  logger.log(Level.WARNING, String.format("Unable to extract emails for path: %s file ID: %d ", path, fileID), ex);
560  }
561  }
562  }
563 
564  @Override
565  public boolean hasNext() {
566  return nextMsg != null;
567  }
568 
569  @Override
570  public EmailMessage next() {
571 
572  currentMsg = nextMsg;
573 
574  try {
575  PSTMessage message = (PSTMessage) folder.getNextChild();
576  if (message != null) {
577  if (wholeMsg) {
578  nextMsg = extractEmailMessage(message, currentPath, fileID);
579  } else {
580  nextMsg = extractPartialEmailMessage(message);
581  }
582  } else {
583  nextMsg = null;
584  }
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);
587  failureCount++;
588  nextMsg = null;
589  }
590 
591  return currentMsg;
592  }
593 
599  Iterable<EmailMessage> getIterable() {
600  return new Iterable<EmailMessage>() {
601  @Override
602  public Iterator<EmailMessage> iterator() {
603  return PstEmailIterator.this;
604  }
605  };
606  }
607 
608  }
609 }

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