19 package org.sleuthkit.autopsy.thunderbirdparser;
 
   21 import ezvcard.Ezvcard;
 
   23 import ezvcard.parameter.EmailType;
 
   24 import ezvcard.parameter.TelephoneType;
 
   25 import ezvcard.property.Email;
 
   26 import ezvcard.property.Organization;
 
   27 import ezvcard.property.Photo;
 
   28 import ezvcard.property.Telephone;
 
   29 import ezvcard.property.Url;
 
   31 import java.io.FileOutputStream;
 
   32 import java.io.IOException;
 
   33 import java.nio.file.Paths;
 
   34 import java.util.ArrayList;
 
   35 import java.util.Arrays;
 
   36 import java.util.Collection;
 
   37 import java.util.HashMap;
 
   38 import java.util.List;
 
   40 import java.util.logging.Level;
 
   41 import org.apache.commons.lang3.StringUtils;
 
   42 import org.openide.util.NbBundle;
 
   56 import org.
sleuthkit.datamodel.Blackboard.BlackboardException;
 
   73 final class VcardParser {
 
   74     private static final String VCARD_HEADER = 
"BEGIN:VCARD";
 
   75     private static final long MIN_FILE_SIZE = 22;
 
   77     private static final String PHOTO_TYPE_BMP = 
"bmp";
 
   78     private static final String PHOTO_TYPE_GIF = 
"gif";
 
   79     private static final String PHOTO_TYPE_JPEG = 
"jpeg";
 
   80     private static final String PHOTO_TYPE_PNG = 
"png";
 
   81     private static final Map<String, String> photoTypeExtensions;
 
   83         photoTypeExtensions = 
new HashMap<>();
 
   84         photoTypeExtensions.put(PHOTO_TYPE_BMP, 
".bmp");
 
   85         photoTypeExtensions.put(PHOTO_TYPE_GIF, 
".gif");
 
   86         photoTypeExtensions.put(PHOTO_TYPE_JPEG, 
".jpg");
 
   87         photoTypeExtensions.put(PHOTO_TYPE_PNG, 
".png");
 
   90     private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
 
   92     private final IngestServices services = IngestServices.getInstance();
 
   93     private final FileManager fileManager;
 
   94     private final IngestJobContext context;
 
   95     private final Blackboard blackboard;
 
   96     private final Case currentCase;
 
   97     private final SleuthkitCase tskCase;
 
  102     VcardParser(Case currentCase, IngestJobContext context) {
 
  103         this.context = context;
 
  104         this.currentCase = currentCase;
 
  105         tskCase = currentCase.getSleuthkitCase();
 
  106         blackboard = tskCase.getBlackboard();
 
  107         fileManager = currentCase.getServices().getFileManager();
 
  117     static boolean isVcardFile(Content content) {
 
  119             if (content.getSize() > MIN_FILE_SIZE) {
 
  120                 byte[] buffer = 
new byte[VCARD_HEADER.length()];
 
  121                 int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
 
  123                     String header = 
new String(buffer);
 
  124                     return header.equalsIgnoreCase(VCARD_HEADER);
 
  127         } 
catch (TskException ex) {
 
  128             logger.log(Level.WARNING, String.format(
"Exception while detecting if the file '%s' (id=%d) is a vCard file.",
 
  129                     content.getName(), content.getId())); 
 
  146     void parse(AbstractFile abstractFile) 
throws IOException, NoCurrentCaseException {
 
  147         for (VCard vcard: Ezvcard.parse(
new ReadContentInputStream(abstractFile)).all()) {
 
  148             addContactArtifact(vcard, abstractFile);
 
  164     @NbBundle.Messages({
"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
 
  165     private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile) 
throws NoCurrentCaseException {
 
  166         List<BlackboardAttribute> attributes = 
new ArrayList<>();
 
  167         List<AccountFileInstance> accountInstances = 
new ArrayList<>();
 
  170         if (vcard.getFormattedName() != null) {
 
  171             name = vcard.getFormattedName().getValue();
 
  173             if (vcard.getStructuredName() != null) {
 
  175                 for (String prefix:vcard.getStructuredName().getPrefixes()) {
 
  176                     name += prefix + 
" ";
 
  178                 if (vcard.getStructuredName().getGiven() != null) {
 
  179                     name += vcard.getStructuredName().getGiven() + 
" ";
 
  181                 if (vcard.getStructuredName().getFamily() != null) {
 
  182                     name += vcard.getStructuredName().getFamily() + 
" ";
 
  184                 for (String suffix:vcard.getStructuredName().getSuffixes()) {
 
  185                     name += suffix + 
" ";
 
  187                 if (! vcard.getStructuredName().getAdditionalNames().isEmpty()) {
 
  189                     for (String addName:vcard.getStructuredName().getAdditionalNames()) {
 
  190                         name += addName + 
" ";
 
  196         ThunderbirdMboxFileIngestModule.addArtifactAttribute(name, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, attributes);
 
  198         for (Telephone telephone : vcard.getTelephoneNumbers()) {
 
  199             addPhoneAttributes(telephone, abstractFile, attributes);
 
  200             addPhoneAccountInstances(telephone, abstractFile, accountInstances);
 
  203         for (Email email : vcard.getEmails()) {
 
  204             addEmailAttributes(email, abstractFile, attributes);
 
  205             addEmailAccountInstances(email, abstractFile, accountInstances);
 
  208         for (Url url : vcard.getUrls()) {
 
  209             ThunderbirdMboxFileIngestModule.addArtifactAttribute(url.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, attributes);
 
  212         for (Organization organization : vcard.getOrganizations()) {
 
  213             List<String> values = organization.getValues();
 
  214             if (values.isEmpty() == 
false) {
 
  215                 ThunderbirdMboxFileIngestModule.addArtifactAttribute(values.get(0), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ORGANIZATION, attributes);
 
  219         AccountFileInstance deviceAccountInstance = addDeviceAccountInstance(abstractFile);
 
  221         BlackboardArtifact artifact = null;
 
  222         org.
sleuthkit.datamodel.Blackboard tskBlackboard = tskCase.getBlackboard();
 
  225             if (!tskBlackboard.artifactExists(abstractFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT, attributes)) {
 
  226                 artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT);
 
  227                 artifact.addAttributes(attributes);
 
  229                  extractPhotos(vcard, abstractFile, artifact);
 
  232                 if (deviceAccountInstance != null) {
 
  234                         currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(
 
  235                                 deviceAccountInstance, accountInstances, artifact, Relationship.Type.CONTACT, abstractFile.getCrtime());
 
  236                     } 
catch (TskDataException ex) {
 
  237                         logger.log(Level.SEVERE, String.format(
"Failed to create phone and e-mail account relationships (fileName='%s'; fileId=%d; accountId=%d).",
 
  238                                 abstractFile.getName(), abstractFile.getId(), deviceAccountInstance.getAccount().getAccountID()), ex); 
 
  244                     blackboard.postArtifact(artifact,  EmailParserModuleFactory.getModuleName());
 
  245                 } 
catch (Blackboard.BlackboardException ex) {
 
  246                     logger.log(Level.SEVERE, 
"Unable to index blackboard artifact " + artifact.getArtifactID(), ex); 
 
  247                     MessageNotifyUtil.Notify.error(Bundle.VcardParser_addContactArtifact_indexError(), artifact.getDisplayName());
 
  250         } 
catch (TskCoreException ex) {
 
  251             logger.log(Level.SEVERE, String.format(
"Failed to create contact artifact for vCard file '%s' (id=%d).",
 
  252                     abstractFile.getName(), abstractFile.getId()), ex); 
 
  266     private void extractPhotos(VCard vcard, AbstractFile abstractFile, BlackboardArtifact artifact) 
throws NoCurrentCaseException {
 
  267         String parentFileName = getUniqueName(abstractFile);
 
  270             String outputPath = getOutputFolderPath(parentFileName);
 
  271             if (
new File(outputPath).exists()) {
 
  272                 List<Photo> vcardPhotos = vcard.getPhotos();
 
  273                 List<AbstractFile> derivedFilesCreated = 
new ArrayList<>();
 
  274                 for (
int i=0; i < vcardPhotos.size(); i++) {
 
  275                     Photo photo = vcardPhotos.get(i);
 
  277                     if (photo.getUrl() != null) {
 
  282                     String type = photo.getType();
 
  289                     type = type.toLowerCase();
 
  290                     if (type.startsWith(
"image/")) {
 
  291                         type = type.substring(6);
 
  293                     String extension = photoTypeExtensions.get(type);
 
  296                     byte[] data = photo.getData();
 
  297                     String extractedFileName = String.format(
"photo_%d%s", i, extension == null ? 
"" : extension);
 
  298                     String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
 
  300                         writeExtractedImage(extractedFilePath, data);
 
  301                         derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
 
  302                                 abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
 
  303                                 true, artifact, null, EmailParserModuleFactory.getModuleName(), EmailParserModuleFactory.getModuleVersion(), 
"", TskData.EncodingType.NONE));
 
  304                     } 
catch (IOException | TskCoreException ex) {
 
  305                         logger.log(Level.WARNING, String.format(
"Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex); 
 
  308                 if (!derivedFilesCreated.isEmpty()) {
 
  309                     services.fireModuleContentEvent(
new ModuleContentEvent(abstractFile));
 
  310                     context.addFilesToJob(derivedFilesCreated);
 
  314                 logger.log(Level.INFO, String.format(
"Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
 
  315                         abstractFile.getName(), abstractFile.getId())); 
 
  317         } 
catch (SecurityException ex) {
 
  318             logger.log(Level.WARNING, String.format(
"Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
 
  329     private void writeExtractedImage(String outputPath, byte[] data) 
throws IOException {
 
  330         File outputFile = 
new File(outputPath);
 
  331         FileOutputStream outputStream = 
new FileOutputStream(outputFile);
 
  332         outputStream.write(data);
 
  343     private String getUniqueName(AbstractFile file) {
 
  344         return file.getName() + 
"_" + file.getId();
 
  356     private String getFileRelativePath(String parentFileName, String fileName) 
throws NoCurrentCaseException {
 
  358         return Paths.get(getRelModuleOutputPath(), parentFileName, fileName).toString();
 
  371     private String getOutputFolderPath(String parentFileName) 
throws NoCurrentCaseException {
 
  372         String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
 
  373         File outputFilePath = 
new File(outputFolderPath);
 
  374         if (!outputFilePath.exists()) {
 
  375             outputFilePath.mkdirs();
 
  377         return outputFolderPath;
 
  388     private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
 
  389         String telephoneText = telephone.getText();
 
  391         if (telephoneText == null || telephoneText.isEmpty()) {
 
  392             telephoneText =  telephone.getUri().getNumber();
 
  393             if (telephoneText == null || telephoneText.isEmpty()) {
 
  399         List<TelephoneType> telephoneTypes = telephone.getTypes();
 
  400         if (telephoneTypes.isEmpty()) {
 
  401             ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
 
  403             TelephoneType type = telephoneTypes.get(0);
 
  410             List<String> splitTelephoneTypes = Arrays.asList(
 
  411                     type.getValue().toUpperCase().replaceAll(
"\\s+",
"").split(
","));
 
  413             if (splitTelephoneTypes.size() > 0) {
 
  414                 String splitType = splitTelephoneTypes.get(0);
 
  415                 String attributeTypeName = 
"TSK_PHONE_NUMBER";
 
  416                 if (splitType != null && !splitType.isEmpty()) {
 
  417                     attributeTypeName = 
"TSK_PHONE_NUMBER_" + splitType;
 
  421                     BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
 
  422                     if (attributeType == null) {
 
  425                             attributeType = tskCase.getBlackboard().getOrAddAttributeType(attributeTypeName,
 
  426                                     BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
 
  427                                     String.format(
"Phone Number (%s)", StringUtils.capitalize(splitType.toLowerCase())));
 
  429                             ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephoneText, attributeType, attributes);
 
  430                         }
catch (BlackboardException ex) {
 
  431                             logger.log(Level.WARNING, String.format(
"Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
 
  435                 } 
catch (TskCoreException ex) {
 
  436                     logger.log(Level.WARNING, String.format(
"Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
 
  450     private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
 
  451         String emailValue = email.getValue();
 
  452         if (emailValue == null || emailValue.isEmpty()) {
 
  457         List<EmailType> emailTypes = email.getTypes();
 
  458         if (emailTypes.isEmpty()) {
 
  459             ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
 
  461             EmailType type = emailTypes.get(0);                
 
  467            List<String> splitEmailTypes = Arrays.asList(
 
  468                    type.getValue().toUpperCase().replaceAll(
"\\s+",
"").split(
","));
 
  470            if (splitEmailTypes.size() > 0) {
 
  471                String splitType = splitEmailTypes.get(0);
 
  472                String attributeTypeName = 
"TSK_EMAIL_" + splitType;
 
  473                if(splitType.isEmpty()) {
 
  474                    attributeTypeName = 
"TSK_EMAIL";
 
  477                    BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
 
  478                    if (attributeType == null) {
 
  480                        attributeType = tskCase.getBlackboard().getOrAddAttributeType(attributeTypeName, 
 
  481                                BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING, 
 
  482                                String.format(
"Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
 
  484                    ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
 
  485                } 
catch (TskCoreException ex) {
 
  486                    logger.log(Level.SEVERE, String.format(
"Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
 
  487                } 
catch (BlackboardException ex) {
 
  488                    logger.log(Level.SEVERE, String.format(
"Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
 
  503     private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
 
  504         String telephoneText = telephone.getText();
 
  505         if (telephoneText == null || telephoneText.isEmpty()) {
 
  506             telephoneText =  telephone.getUri().getNumber();
 
  507             if (telephoneText == null || telephoneText.isEmpty()) {
 
  515             AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
 
  516                     telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile);
 
  517             accountInstances.add(phoneAccountInstance);
 
  519         catch(TskCoreException ex) {
 
  520              logger.log(Level.WARNING, String.format(
 
  521                      "Failed to create account for phone number '%s' (content='%s'; id=%d).",
 
  522                      telephoneText, abstractFile.getName(), abstractFile.getId()), ex); 
 
  535     private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
 
  536         String emailValue = email.getValue();
 
  537         if (emailValue == null || emailValue.isEmpty()) {
 
  543             AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
 
  544                     emailValue, EmailParserModuleFactory.getModuleName(), abstractFile);
 
  545             accountInstances.add(emailAccountInstance);
 
  547         catch(TskCoreException ex) {
 
  548              logger.log(Level.WARNING, String.format(
 
  549                      "Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
 
  550                      emailValue, abstractFile.getName(), abstractFile.getId()), ex); 
 
  561     private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
 
  563         AccountFileInstance deviceAccountInstance = null;
 
  564         String deviceId = null;
 
  566             long dataSourceObjId = abstractFile.getDataSourceObjectId();
 
  567             DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
 
  568             deviceId = dataSource.getDeviceId();
 
  569             deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
 
  570                     deviceId, EmailParserModuleFactory.getModuleName(), abstractFile);
 
  572         catch (TskCoreException ex) {
 
  573             logger.log(Level.WARNING, String.format(
 
  574                     "Failed to create device account for '%s' (content='%s'; id=%d).",
 
  575                     deviceId, abstractFile.getName(), abstractFile.getId()), ex); 
 
  577         catch (TskDataException ex) {
 
  578             logger.log(Level.WARNING, String.format(
 
  579                     "Failed to get the data source from the case database (id=%d).",
 
  580                     abstractFile.getId()), ex); 
 
  583         return deviceAccountInstance;