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().isEmpty()) ? msg.getSenderEmailAddress() : 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() {