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 if (ex.getMessage().toLowerCase().startsWith(
"unable to")) {
111 logger.log(Level.WARNING, ex.getMessage());
112 logger.log(Level.WARNING, String.format(
"Error in parsing PST file %s, file may be empty or corrupt", file.getName()));
113 return ParseResult.ERROR;
115 String msg = file.getName() +
": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage();
116 logger.log(Level.WARNING, msg, ex);
117 return ParseResult.ERROR;
118 }
catch (IOException ex) {
119 String msg = file.getName() +
": Failed to create internal java-libpst PST file to parse:\n" + ex.getMessage();
120 logger.log(Level.WARNING, msg, ex);
121 return ParseResult.ERROR;
122 }
catch (IllegalArgumentException ex) {
123 logger.log(Level.INFO,
"Found encrypted PST file.");
124 return ParseResult.ENCRYPT;
127 return ParseResult.OK;
131 public void close() throws IOException{
132 if(pstFile != null) {
133 RandomAccessFile file = pstFile.getFileHandle();
146 Iterator<EmailMessage> getEmailMessageIterator() {
147 if (pstFile == null) {
151 Iterable<EmailMessage> iterable = null;
154 iterable = getEmailMessageIterator(pstFile.getRootFolder(),
"\\", fileID,
true);
155 }
catch (PSTException | IOException ex) {
156 logger.log(Level.WARNING, String.format(
"Exception thrown while parsing fileID: %d", fileID), ex);
159 if (iterable == null) {
163 return iterable.iterator();
172 List<EmailMessage> getPartialEmailMessages() {
173 List<EmailMessage> messages =
new ArrayList<>();
174 Iterator<EmailMessage> iterator = getPartialEmailMessageIterator();
175 if (iterator != null) {
176 while (iterator.hasNext()) {
177 messages.add(iterator.next());
191 for (String msg: errorList) {
192 result +=
"<li>" + msg +
"</li>";
202 int getFailureCount() {
213 private Iterator<EmailMessage> getPartialEmailMessageIterator() {
214 if (pstFile == null) {
218 Iterable<EmailMessage> iterable = null;
221 iterable = getEmailMessageIterator(pstFile.getRootFolder(),
"\\", fileID,
false);
222 }
catch (PSTException | IOException ex) {
223 logger.log(Level.WARNING, String.format(
"Exception thrown while parsing fileID: %d", fileID), ex);
226 if (iterable == null) {
230 return iterable.iterator();
247 private Iterable<EmailMessage> getEmailMessageIterator(PSTFolder folder, String path,
long fileID,
boolean wholeMsg)
throws PSTException, IOException {
248 Iterable<EmailMessage> iterable = null;
250 if (folder.getContentCount() > 0) {
251 iterable =
new PstEmailIterator(folder, path, fileID, wholeMsg).getIterable();
254 if (folder.hasSubfolders()) {
255 List<PSTFolder> subFolders = folder.getSubFolders();
256 for (PSTFolder subFolder : subFolders) {
257 String newpath = path +
"\\" + subFolder.getDisplayName();
258 Iterable<EmailMessage> subIterable = getEmailMessageIterator(subFolder, newpath, fileID, wholeMsg);
259 if (subIterable == null) {
263 if (iterable != null) {
264 iterable = Iterables.concat(iterable, subIterable);
266 iterable = subIterable;
283 private EmailMessage extractEmailMessage(PSTMessage msg, String localPath,
long fileID) {
284 EmailMessage email =
new EmailMessage();
285 String toAddress = msg.getDisplayTo();
286 String ccAddress = msg.getDisplayCC();
287 String bccAddress = msg.getDisplayBCC();
288 String receivedByName = msg.getReceivedByName();
289 String receivedBySMTPAddress = msg.getReceivedBySMTPAddress();
291 if (toAddress.contains(receivedByName)) {
292 toAddress = toAddress.replace(receivedByName, receivedBySMTPAddress);
294 if (ccAddress.contains(receivedByName)) {
295 ccAddress = ccAddress.replace(receivedByName, receivedBySMTPAddress);
297 if (bccAddress.contains(receivedByName)) {
298 bccAddress = bccAddress.replace(receivedByName, receivedBySMTPAddress);
300 email.setRecipients(toAddress);
301 email.setCc(ccAddress);
302 email.setBcc(bccAddress);
303 email.setSender(getSender(msg.getSenderName(), msg.getSentRepresentingSMTPAddress()));
304 email.setSentDate(msg.getMessageDeliveryTime());
305 email.setTextBody(msg.getBody());
306 if (
false == msg.getTransportMessageHeaders().isEmpty()) {
307 email.setHeaders(
"\n-----HEADERS-----\n\n" + msg.getTransportMessageHeaders() +
"\n\n---END HEADERS--\n\n");
309 email.setHtmlBody(msg.getBodyHTML());
312 rtf = msg.getRTFBody();
313 }
catch (PSTException | IOException ex) {
314 logger.log(Level.INFO,
"Failed to get RTF content from pst email.");
316 email.setRtfBody(rtf);
317 email.setLocalPath(localPath);
318 email.setSubject(msg.getSubject());
319 email.setId(msg.getDescriptorNodeId());
320 email.setMessageID(msg.getInternetMessageId());
322 String inReplyToID = msg.getInReplyToId();
323 email.setInReplyToID(inReplyToID);
325 if (msg.hasAttachments()) {
326 extractAttachments(email, msg, fileID);
329 List<String> references = extractReferences(msg.getTransportMessageHeaders());
330 if (inReplyToID != null && !inReplyToID.isEmpty()) {
331 if (references == null) {
332 references =
new ArrayList<>();
333 references.add(inReplyToID);
334 }
else if (!references.contains(inReplyToID)) {
335 references.add(inReplyToID);
338 email.setReferences(references);
350 private EmailMessage extractPartialEmailMessage(PSTMessage msg) {
351 EmailMessage email =
new EmailMessage();
352 email.setSubject(msg.getSubject());
353 email.setId(msg.getDescriptorNodeId());
354 email.setMessageID(msg.getInternetMessageId());
355 String inReplyToID = msg.getInReplyToId();
356 email.setInReplyToID(inReplyToID);
357 List<String> references = extractReferences(msg.getTransportMessageHeaders());
358 if (inReplyToID != null && !inReplyToID.isEmpty()) {
359 if (references == null) {
360 references =
new ArrayList<>();
361 references.add(inReplyToID);
362 }
else if (!references.contains(inReplyToID)) {
363 references.add(inReplyToID);
366 email.setReferences(references);
377 @NbBundle.Messages({
"PstParser.noOpenCase.errMsg=Exception while getting open case."})
378 private void extractAttachments(EmailMessage email, PSTMessage msg,
long fileID) {
379 int numberOfAttachments = msg.getNumberOfAttachments();
380 String outputDirPath;
382 outputDirPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator;
383 }
catch (NoCurrentCaseException ex) {
384 logger.log(Level.SEVERE,
"Exception while getting open case.", ex);
387 for (
int x = 0; x < numberOfAttachments; x++) {
388 String filename =
"";
390 PSTAttachment attach = msg.getAttachment(x);
391 long size = attach.getAttachSize();
392 long freeSpace = services.getFreeDiskSpace();
393 if ((freeSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && (size >= freeSpace)) {
397 filename = attach.getLongFilename();
398 if (filename.isEmpty()) {
399 filename = attach.getFilename();
401 String uniqueFilename = fileID +
"-" + msg.getDescriptorNodeId() +
"-" + attach.getContentId() +
"-" + FileUtil.escapeFileName(filename);
402 String outPath = outputDirPath + uniqueFilename;
403 saveAttachmentToDisk(attach, outPath);
405 EmailMessage.Attachment attachment =
new EmailMessage.Attachment();
407 long crTime = attach.getCreationTime() != null ? attach.getCreationTime().getTime() / 1000 : 0;
408 long mTime = attach.getModificationTime() != null ? attach.getModificationTime().getTime() / 1000 : 0;
409 String relPath = getRelModuleOutputPath() + File.separator + uniqueFilename;
410 attachment.setName(filename);
411 attachment.setCrTime(crTime);
412 attachment.setmTime(mTime);
413 attachment.setLocalPath(relPath);
414 attachment.setSize(attach.getFilesize());
415 attachment.setEncodingType(TskData.EncodingType.XOR1);
416 email.addAttachment(attachment);
417 }
catch (PSTException | IOException | NullPointerException ex) {
423 NbBundle.getMessage(
this.getClass(),
"PstParser.extractAttch.errMsg.failedToExtractToDisk",
425 logger.log(Level.WARNING,
"Failed to extract attachment from pst file.", ex);
426 }
catch (NoCurrentCaseException ex) {
427 addErrorMessage(Bundle.PstParser_noOpenCase_errMsg());
428 logger.log(Level.SEVERE, Bundle.PstParser_noOpenCase_errMsg(), ex);
442 private void saveAttachmentToDisk(PSTAttachment attach, String outPath)
throws IOException, PSTException {
443 try (InputStream attachmentStream = attach.getFileInputStream();
444 EncodedFileOutputStream out =
new EncodedFileOutputStream(
new FileOutputStream(outPath), TskData.EncodingType.XOR1)) {
446 int bufferSize = 8176;
447 byte[] buffer =
new byte[bufferSize];
448 int count = attachmentStream.read(buffer);
451 throw new IOException(
"attachmentStream invalid (read() fails). File " + attach.getLongFilename() +
" skipped");
454 while (count == bufferSize) {
456 count = attachmentStream.read(buffer);
459 byte[] endBuffer =
new byte[count];
460 System.arraycopy(buffer, 0, endBuffer, 0, count);
461 out.write(endBuffer);
474 private String getSender(String name, String addr) {
475 if (name.isEmpty() && addr.isEmpty()) {
477 }
else if (name.isEmpty()) {
479 }
else if (addr.isEmpty()) {
482 return name +
": " + addr;
493 public static boolean isPstFile(AbstractFile file) {
494 byte[] buffer =
new byte[4];
496 int read = file.read(buffer, 0, 4);
500 ByteBuffer bb = ByteBuffer.wrap(buffer);
501 return bb.getInt() == PST_HEADER;
502 }
catch (TskCoreException ex) {
503 logger.log(Level.WARNING,
"Exception while detecting if a file is a pst file.");
513 private void addErrorMessage(String msg) {
524 private List<String> extractReferences(String emailHeader) {
525 Scanner scanner =
new Scanner(emailHeader);
526 StringBuilder buffer = null;
527 while (scanner.hasNextLine()) {
528 String token = scanner.nextLine();
530 if (token.matches(
"^References:.*")) {
531 buffer =
new StringBuilder();
532 buffer.append((token.substring(token.indexOf(
':') + 1)).trim());
533 }
else if (buffer != null) {
534 if (token.matches(
"^\\w+:.*$")) {
535 List<String> references =
new ArrayList<>();
536 for (String
id : buffer.toString().split(
">")) {
537 references.add(
id.trim() +
">");
541 buffer.append(token.trim());
571 PstEmailIterator(PSTFolder folder, String path,
long fileID,
boolean wholeMsg) {
574 this.currentPath = path;
577 if (folder.getContentCount() > 0) {
579 PSTMessage message = (PSTMessage) folder.getNextChild();
580 if (message != null) {
582 nextMsg = extractEmailMessage(message, currentPath, fileID);
584 nextMsg = extractPartialEmailMessage(message);
587 }
catch (PSTException | IOException ex) {
589 logger.log(Level.WARNING, String.format(
"Unable to extract emails for path: %s file ID: %d ", path, fileID), ex);
596 return nextMsg != null;
605 PSTMessage message = (PSTMessage) folder.getNextChild();
606 if (message != null) {
608 nextMsg = extractEmailMessage(message, currentPath, fileID);
610 nextMsg = extractPartialEmailMessage(message);
615 }
catch (PSTException | IOException ex) {
616 logger.log(Level.WARNING, String.format(
"Unable to extract emails for path: %s file ID: %d ", currentPath, fileID), ex);
629 Iterable<EmailMessage> getIterable() {
630 return new Iterable<EmailMessage>() {
632 public Iterator<EmailMessage> iterator() {