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;
30 import java.io.BufferedInputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStreamReader;
35 import java.nio.charset.StandardCharsets;
36 import java.nio.file.Paths;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collection;
40 import java.util.HashMap;
41 import java.util.List;
43 import java.util.concurrent.ConcurrentMap;
44 import java.util.logging.Level;
45 import org.apache.commons.lang3.StringUtils;
46 import org.openide.util.NbBundle;
60 import org.
sleuthkit.datamodel.Blackboard.BlackboardException;
77 final class VcardParser {
78 private static final String VCARD_HEADER =
"BEGIN:VCARD";
79 private static final long MIN_FILE_SIZE = 22;
81 private static final String PHOTO_TYPE_BMP =
"bmp";
82 private static final String PHOTO_TYPE_GIF =
"gif";
83 private static final String PHOTO_TYPE_JPEG =
"jpeg";
84 private static final String PHOTO_TYPE_PNG =
"png";
85 private static final Map<String, String> photoTypeExtensions;
87 photoTypeExtensions =
new HashMap<>();
88 photoTypeExtensions.put(PHOTO_TYPE_BMP,
".bmp");
89 photoTypeExtensions.put(PHOTO_TYPE_GIF,
".gif");
90 photoTypeExtensions.put(PHOTO_TYPE_JPEG,
".jpg");
91 photoTypeExtensions.put(PHOTO_TYPE_PNG,
".png");
94 private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
96 private final IngestServices services = IngestServices.getInstance();
97 private final FileManager fileManager;
98 private final IngestJobContext context;
99 private final Blackboard blackboard;
100 private final Case currentCase;
101 private final SleuthkitCase tskCase;
106 private final ConcurrentMap<String, BlackboardAttribute.Type> customAttributeCache;
111 VcardParser(Case currentCase, IngestJobContext context, ConcurrentMap<String, BlackboardAttribute.Type> customAttributeCache) {
112 this.context = context;
113 this.currentCase = currentCase;
114 tskCase = currentCase.getSleuthkitCase();
115 blackboard = tskCase.getBlackboard();
116 fileManager = currentCase.getServices().getFileManager();
117 this.customAttributeCache = customAttributeCache;
127 static boolean isVcardFile(Content content) {
129 if (content.getSize() > MIN_FILE_SIZE) {
130 byte[] buffer =
new byte[VCARD_HEADER.length()];
131 int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
133 String header =
new String(buffer);
134 return header.equalsIgnoreCase(VCARD_HEADER);
137 }
catch (TskException ex) {
138 logger.log(Level.WARNING, String.format(
"Exception while detecting if the file '%s' (id=%d) is a vCard file.",
139 content.getName(), content.getId()));
156 void parse(AbstractFile abstractFile)
throws IOException, NoCurrentCaseException {
157 for (VCard vcard: Ezvcard.parse(
new InputStreamReader(
new BufferedInputStream(
new ReadContentInputStream(abstractFile)), StandardCharsets.UTF_8)).all()) {
158 addContactArtifact(vcard, abstractFile);
174 @NbBundle.Messages({
"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
175 private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile)
throws NoCurrentCaseException {
176 List<BlackboardAttribute> attributes =
new ArrayList<>();
177 List<AccountFileInstance> accountInstances =
new ArrayList<>();
180 if (vcard.getFormattedName() != null) {
181 name = vcard.getFormattedName().getValue();
183 if (vcard.getStructuredName() != null) {
185 for (String prefix:vcard.getStructuredName().getPrefixes()) {
186 name += prefix +
" ";
188 if (vcard.getStructuredName().getGiven() != null) {
189 name += vcard.getStructuredName().getGiven() +
" ";
191 if (vcard.getStructuredName().getFamily() != null) {
192 name += vcard.getStructuredName().getFamily() +
" ";
194 for (String suffix:vcard.getStructuredName().getSuffixes()) {
195 name += suffix +
" ";
197 if (! vcard.getStructuredName().getAdditionalNames().isEmpty()) {
199 for (String addName:vcard.getStructuredName().getAdditionalNames()) {
200 name += addName +
" ";
206 ThunderbirdMboxFileIngestModule.addArtifactAttribute(name, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, attributes);
208 for (Telephone telephone : vcard.getTelephoneNumbers()) {
209 addPhoneAttributes(telephone, abstractFile, attributes);
210 addPhoneAccountInstances(telephone, abstractFile, accountInstances);
213 for (Email email : vcard.getEmails()) {
214 addEmailAttributes(email, abstractFile, attributes);
215 addEmailAccountInstances(email, abstractFile, accountInstances);
218 for (Url url : vcard.getUrls()) {
219 ThunderbirdMboxFileIngestModule.addArtifactAttribute(url.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, attributes);
222 for (Organization organization : vcard.getOrganizations()) {
223 List<String> values = organization.getValues();
224 if (values.isEmpty() ==
false) {
225 ThunderbirdMboxFileIngestModule.addArtifactAttribute(values.get(0), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ORGANIZATION, attributes);
229 AccountFileInstance deviceAccountInstance = addDeviceAccountInstance(abstractFile);
231 BlackboardArtifact artifact = null;
232 org.
sleuthkit.datamodel.Blackboard tskBlackboard = tskCase.getBlackboard();
235 if (!tskBlackboard.artifactExists(abstractFile, BlackboardArtifact.Type.TSK_CONTACT, attributes)) {
236 artifact = abstractFile.newDataArtifact(
new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT), attributes);
238 extractPhotos(vcard, abstractFile, artifact);
241 if (deviceAccountInstance != null) {
243 currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(
244 deviceAccountInstance, accountInstances, artifact, Relationship.Type.CONTACT, abstractFile.getCrtime());
245 }
catch (TskDataException ex) {
246 logger.log(Level.SEVERE, String.format(
"Failed to create phone and e-mail account relationships (fileName='%s'; fileId=%d; accountId=%d).",
247 abstractFile.getName(), abstractFile.getId(), deviceAccountInstance.getAccount().getAccountID()), ex);
253 blackboard.postArtifact(artifact, EmailParserModuleFactory.getModuleName(), context.getJobId());
254 }
catch (Blackboard.BlackboardException ex) {
255 logger.log(Level.SEVERE,
"Unable to index blackboard artifact " + artifact.getArtifactID(), ex);
256 MessageNotifyUtil.Notify.error(Bundle.VcardParser_addContactArtifact_indexError(), artifact.getDisplayName());
259 }
catch (TskCoreException ex) {
260 logger.log(Level.SEVERE, String.format(
"Failed to create contact artifact for vCard file '%s' (id=%d).",
261 abstractFile.getName(), abstractFile.getId()), ex);
275 private void extractPhotos(VCard vcard, AbstractFile abstractFile, BlackboardArtifact artifact)
throws NoCurrentCaseException {
276 String parentFileName = getUniqueName(abstractFile);
279 String outputPath = getOutputFolderPath(parentFileName);
280 if (
new File(outputPath).exists()) {
281 List<Photo> vcardPhotos = vcard.getPhotos();
282 List<AbstractFile> derivedFilesCreated =
new ArrayList<>();
283 for (
int i=0; i < vcardPhotos.size(); i++) {
284 Photo photo = vcardPhotos.get(i);
286 if (photo.getUrl() != null) {
291 String type = photo.getType();
298 type = type.toLowerCase();
299 if (type.startsWith(
"image/")) {
300 type = type.substring(6);
302 String extension = photoTypeExtensions.get(type);
305 byte[] data = photo.getData();
306 String extractedFileName = String.format(
"photo_%d%s", i, extension == null ?
"" : extension);
307 String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
309 writeExtractedImage(extractedFilePath, data);
310 derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
311 abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
312 true, artifact, null, EmailParserModuleFactory.getModuleName(), EmailParserModuleFactory.getModuleVersion(),
"", TskData.EncodingType.NONE));
313 }
catch (IOException | TskCoreException ex) {
314 logger.log(Level.WARNING, String.format(
"Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex);
317 if (!derivedFilesCreated.isEmpty()) {
318 services.fireModuleContentEvent(
new ModuleContentEvent(abstractFile));
319 context.addFilesToJob(derivedFilesCreated);
323 logger.log(Level.INFO, String.format(
"Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
324 abstractFile.getName(), abstractFile.getId()));
326 }
catch (SecurityException ex) {
327 logger.log(Level.WARNING, String.format(
"Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
338 private void writeExtractedImage(String outputPath, byte[] data)
throws IOException {
339 File outputFile =
new File(outputPath);
340 FileOutputStream outputStream =
new FileOutputStream(outputFile);
341 outputStream.write(data);
352 private String getUniqueName(AbstractFile file) {
353 return file.getName() +
"_" + file.getId();
365 private String getFileRelativePath(String parentFileName, String fileName)
throws NoCurrentCaseException {
367 return Paths.get(getRelModuleOutputPath(), parentFileName, fileName).toString();
380 private String getOutputFolderPath(String parentFileName)
throws NoCurrentCaseException {
381 String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
382 File outputFilePath =
new File(outputFolderPath);
383 if (!outputFilePath.exists()) {
384 outputFilePath.mkdirs();
386 return outputFolderPath;
397 private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
398 String telephoneText = telephone.getText();
400 if (telephoneText == null || telephoneText.isEmpty()) {
401 if (telephone.getUri() == null) {
404 telephoneText = telephone.getUri().getNumber();
405 if (telephoneText == null || telephoneText.isEmpty()) {
411 List<TelephoneType> telephoneTypes = telephone.getTypes();
412 if (telephoneTypes.isEmpty()) {
413 ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
415 TelephoneType type = telephoneTypes.get(0);
422 List<String> splitTelephoneTypes = Arrays.asList(
423 type.getValue().toUpperCase().replaceAll(
"\\s+",
"").split(
","));
425 if (splitTelephoneTypes.size() > 0) {
426 String splitType = splitTelephoneTypes.get(0);
427 String attributeTypeName =
"TSK_PHONE_NUMBER";
428 if (splitType != null && !splitType.isEmpty()) {
429 attributeTypeName =
"TSK_PHONE_NUMBER_" + splitType;
432 final String finalAttrTypeName = attributeTypeName;
435 BlackboardAttribute.Type attributeType
436 = this.customAttributeCache.computeIfAbsent(finalAttrTypeName, k -> {
439 return tskCase.getBlackboard().getOrAddAttributeType(finalAttrTypeName,
440 BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
441 String.format(
"Phone Number (%s)", StringUtils.capitalize(splitType.toLowerCase())));
443 }
catch (BlackboardException ex) {
444 VcardParser.logger.log(Level.WARNING, String.format(
"Unable to retrieve attribute type '%s' for file '%s' (id=%d).",
445 finalAttrTypeName, abstractFile.getName(), abstractFile.getId()), ex);
450 if (attributeType != null) {
451 ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephoneText, attributeType, attributes);
465 private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
466 String emailValue = email.getValue();
467 if (emailValue == null || emailValue.isEmpty()) {
472 List<EmailType> emailTypes = email.getTypes();
473 if (emailTypes.isEmpty()) {
474 ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
476 EmailType type = emailTypes.get(0);
482 List<String> splitEmailTypes = Arrays.asList(
483 type.getValue().toUpperCase().replaceAll(
"\\s+",
"").split(
","));
485 if (splitEmailTypes.size() > 0) {
486 String splitType = splitEmailTypes.get(0);
487 String attributeTypeName =
"TSK_EMAIL_" + splitType;
488 if (splitType.isEmpty()) {
489 attributeTypeName =
"TSK_EMAIL";
492 final String finalAttributeTypeName = attributeTypeName;
494 BlackboardAttribute.Type attributeType
495 = this.customAttributeCache.computeIfAbsent(finalAttributeTypeName, k -> {
498 return tskCase.getBlackboard().getOrAddAttributeType(finalAttributeTypeName,
499 BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
500 String.format(
"Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
501 }
catch (BlackboardException ex) {
502 logger.log(Level.SEVERE, String.format(
"Unable to add custom attribute type '%s' for file '%s' (id=%d).",
503 finalAttributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
509 if (attributeType != null) {
510 ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
525 private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
526 String telephoneText = telephone.getText();
527 if (telephoneText == null || telephoneText.isEmpty()) {
528 if (telephone.getUri() == null) {
531 telephoneText = telephone.getUri().getNumber();
532 if (telephoneText == null || telephoneText.isEmpty()) {
540 AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
541 telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile, null, context.getJobId());
542 accountInstances.add(phoneAccountInstance);
544 catch(TskCoreException ex) {
545 logger.log(Level.WARNING, String.format(
546 "Failed to create account for phone number '%s' (content='%s'; id=%d).",
547 telephoneText, abstractFile.getName(), abstractFile.getId()), ex);
560 private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
561 String emailValue = email.getValue();
562 if (emailValue == null || emailValue.isEmpty()) {
568 AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
569 emailValue, EmailParserModuleFactory.getModuleName(), abstractFile, null, context.getJobId());
570 accountInstances.add(emailAccountInstance);
572 catch(TskCoreException ex) {
573 logger.log(Level.WARNING, String.format(
574 "Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
575 emailValue, abstractFile.getName(), abstractFile.getId()), ex);
586 private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
588 AccountFileInstance deviceAccountInstance = null;
589 String deviceId = null;
591 long dataSourceObjId = abstractFile.getDataSourceObjectId();
592 DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
593 deviceId = dataSource.getDeviceId();
594 deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
595 deviceId, EmailParserModuleFactory.getModuleName(), abstractFile, null, context.getJobId());
597 catch (TskCoreException ex) {
598 logger.log(Level.WARNING, String.format(
599 "Failed to create device account for '%s' (content='%s'; id=%d).",
600 deviceId, abstractFile.getName(), abstractFile.getId()), ex);
602 catch (TskDataException ex) {
603 logger.log(Level.WARNING, String.format(
604 "Failed to get the data source from the case database (id=%d).",
605 abstractFile.getId()), ex);
608 return deviceAccountInstance;