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.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;
55 class PstParser
implements AutoCloseable{
57 private static final Logger logger = Logger.getLogger(PstParser.class.getName());
61 private static int PST_HEADER = 0x2142444E;
63 private final IngestServices services;
65 private PSTFile pstFile;
68 private int failureCount = 0;
70 private final List<String> errorList =
new ArrayList<>();
72 PstParser(IngestServices services) {
73 this.services = services;
96 ParseResult open(File file,
long fileID) {
98 return ParseResult.ERROR;
102 pstFile =
new PSTFile(file);
103 }
catch (PSTException ex) {
106 if (ex.getMessage().equals(
"Only unencrypted and compressable PST files are supported at this time")) {
107 logger.log(Level.INFO,
"Found encrypted PST file.");
108 return ParseResult.ENCRYPT;
110 String msg = file.getName() +
": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage();
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();
115 logger.log(Level.WARNING, msg, ex);
116 return ParseResult.ERROR;
117 }
catch (IllegalArgumentException ex) {
118 logger.log(Level.INFO,
"Found encrypted PST file.");
119 return ParseResult.ENCRYPT;
122 return ParseResult.OK;
126 public void close() throws Exception{
127 if(pstFile != null) {
128 RandomAccessFile file = pstFile.getFileHandle();
141 Iterator<EmailMessage> getEmailMessageIterator() {
142 if (pstFile == null) {
146 Iterable<EmailMessage> iterable = null;
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);
154 if (iterable == null) {
158 return iterable.iterator();
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());
186 for (String msg: errorList) {
187 result +=
"<li>" + msg +
"</li>";
197 int getFailureCount() {
208 private Iterator<EmailMessage> getPartialEmailMessageIterator() {
209 if (pstFile == null) {
213 Iterable<EmailMessage> iterable = null;
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);
221 if (iterable == null) {
225 return iterable.iterator();
242 private Iterable<EmailMessage> getEmailMessageIterator(PSTFolder folder, String path,
long fileID,
boolean wholeMsg)
throws PSTException, IOException {
243 Iterable<EmailMessage> iterable = null;
245 if (folder.getContentCount() > 0) {
246 iterable =
new PstEmailIterator(folder, path, fileID, wholeMsg).getIterable();
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) {
258 if (iterable != null) {
259 iterable = Iterables.concat(iterable, subIterable);
261 iterable = subIterable;
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");
290 email.setHtmlBody(msg.getBodyHTML());
293 rtf = msg.getRTFBody();
294 }
catch (PSTException | IOException ex) {
295 logger.log(Level.INFO,
"Failed to get RTF content from pst email.");
297 email.setRtfBody(rtf);
298 email.setLocalPath(localPath);
299 email.setSubject(msg.getSubject());
300 email.setId(msg.getDescriptorNodeId());
301 email.setMessageID(msg.getInternetMessageId());
303 String inReplyToID = msg.getInReplyToId();
304 email.setInReplyToID(inReplyToID);
306 if (msg.hasAttachments()) {
307 extractAttachments(email, msg, fileID);
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);
319 email.setReferences(references);
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);
347 email.setReferences(references);
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;
363 outputDirPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator;
364 }
catch (NoCurrentCaseException ex) {
365 logger.log(Level.SEVERE,
"Exception while getting open case.", ex);
368 for (
int x = 0; x < numberOfAttachments; x++) {
369 String filename =
"";
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)) {
378 filename = attach.getLongFilename();
379 if (filename.isEmpty()) {
380 filename = attach.getFilename();
382 String uniqueFilename = fileID +
"-" + msg.getDescriptorNodeId() +
"-" + attach.getContentId() +
"-" + FileUtil.escapeFileName(filename);
383 String outPath = outputDirPath + uniqueFilename;
384 saveAttachmentToDisk(attach, outPath);
386 EmailMessage.Attachment attachment =
new EmailMessage.Attachment();
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) {
404 NbBundle.getMessage(
this.getClass(),
"PstParser.extractAttch.errMsg.failedToExtractToDisk",
406 logger.log(Level.WARNING,
"Failed to extract attachment from pst file.", ex);
407 }
catch (NoCurrentCaseException ex) {
408 addErrorMessage(Bundle.PstParser_noOpenCase_errMsg());
409 logger.log(Level.SEVERE, Bundle.PstParser_noOpenCase_errMsg(), ex);
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)) {
427 int bufferSize = 8176;
428 byte[] buffer =
new byte[bufferSize];
429 int count = attachmentStream.read(buffer);
432 throw new IOException(
"attachmentStream invalid (read() fails). File " + attach.getLongFilename() +
" skipped");
435 while (count == bufferSize) {
437 count = attachmentStream.read(buffer);
440 byte[] endBuffer =
new byte[count];
441 System.arraycopy(buffer, 0, endBuffer, 0, count);
442 out.write(endBuffer);
455 private String getSender(String name, String addr) {
456 if (name.isEmpty() && addr.isEmpty()) {
458 }
else if (name.isEmpty()) {
460 }
else if (addr.isEmpty()) {
463 return name +
": " + addr;
474 public static boolean isPstFile(AbstractFile file) {
475 byte[] buffer =
new byte[4];
477 int read = file.read(buffer, 0, 4);
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.");
494 private void addErrorMessage(String msg) {
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();
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() +
">");
522 buffer.append(token.trim());
552 PstEmailIterator(PSTFolder folder, String path,
long fileID,
boolean wholeMsg) {
555 this.currentPath = path;
558 if (folder.getContentCount() > 0) {
560 PSTMessage message = (PSTMessage) folder.getNextChild();
561 if (message != null) {
563 nextMsg = extractEmailMessage(message, currentPath, fileID);
565 nextMsg = extractPartialEmailMessage(message);
568 }
catch (PSTException | IOException ex) {
570 logger.log(Level.WARNING, String.format(
"Unable to extract emails for path: %s file ID: %d ", path, fileID), ex);
577 return nextMsg != null;
586 PSTMessage message = (PSTMessage) folder.getNextChild();
587 if (message != null) {
589 nextMsg = extractEmailMessage(message, currentPath, fileID);
591 nextMsg = extractPartialEmailMessage(message);
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);
610 Iterable<EmailMessage> getIterable() {
611 return new Iterable<EmailMessage>() {
613 public Iterator<EmailMessage> iterator() {