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;
72 final class VcardParser {
73 private static final String VCARD_HEADER =
"BEGIN:VCARD";
74 private static final long MIN_FILE_SIZE = 22;
76 private static final String PHOTO_TYPE_BMP =
"bmp";
77 private static final String PHOTO_TYPE_GIF =
"gif";
78 private static final String PHOTO_TYPE_JPEG =
"jpeg";
79 private static final String PHOTO_TYPE_PNG =
"png";
80 private static final Map<String, String> photoTypeExtensions;
82 photoTypeExtensions =
new HashMap<>();
83 photoTypeExtensions.put(PHOTO_TYPE_BMP,
".bmp");
84 photoTypeExtensions.put(PHOTO_TYPE_GIF,
".gif");
85 photoTypeExtensions.put(PHOTO_TYPE_JPEG,
".jpg");
86 photoTypeExtensions.put(PHOTO_TYPE_PNG,
".png");
89 private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
91 private final IngestServices services = IngestServices.getInstance();
92 private final FileManager fileManager;
93 private final IngestJobContext context;
94 private final Blackboard blackboard;
95 private final Case currentCase;
96 private final SleuthkitCase tskCase;
101 VcardParser(Case currentCase, IngestJobContext context) {
102 this.context = context;
103 this.currentCase = currentCase;
104 tskCase = currentCase.getSleuthkitCase();
105 blackboard = currentCase.getServices().getBlackboard();
106 fileManager = currentCase.getServices().getFileManager();
116 static boolean isVcardFile(Content content) {
118 if (content.getSize() > MIN_FILE_SIZE) {
119 byte[] buffer =
new byte[VCARD_HEADER.length()];
120 int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
122 String header =
new String(buffer);
123 return header.equalsIgnoreCase(VCARD_HEADER);
126 }
catch (TskException ex) {
127 logger.log(Level.WARNING, String.format(
"Exception while detecting if the file '%s' (id=%d) is a vCard file.",
128 content.getName(), content.getId()));
145 void parse(File vcardFile, AbstractFile abstractFile)
throws IOException, NoCurrentCaseException {
146 VCard vcard = Ezvcard.parse(vcardFile).first();
147 addContactArtifact(vcard, abstractFile);
162 @NbBundle.Messages({
"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
163 private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile)
throws NoCurrentCaseException {
164 List<BlackboardAttribute> attributes =
new ArrayList<>();
165 List<AccountFileInstance> accountInstances =
new ArrayList<>();
167 extractPhotos(vcard, abstractFile);
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_PERSON, 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);
228 List<BlackboardArtifact> blackboardArtifacts =
new ArrayList<>();
229 blackboardArtifacts.add(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.indexArtifact(artifact);
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());
251 IngestServices.getInstance().fireModuleDataEvent(
new ModuleDataEvent(
252 EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT,
253 blackboardArtifacts));
255 }
catch (TskCoreException ex) {
256 logger.log(Level.SEVERE, String.format(
"Failed to create contact artifact for vCard file '%s' (id=%d).",
257 abstractFile.getName(), abstractFile.getId()), ex);
271 private void extractPhotos(VCard vcard, AbstractFile abstractFile)
throws NoCurrentCaseException {
272 String parentFileName = getUniqueName(abstractFile);
275 String outputPath = getOutputFolderPath(parentFileName);
276 if (
new File(outputPath).exists()) {
277 List<Photo> vcardPhotos = vcard.getPhotos();
278 List<AbstractFile> derivedFilesCreated =
new ArrayList<>();
279 for (
int i=0; i < vcardPhotos.size(); i++) {
280 Photo photo = vcardPhotos.get(i);
282 if (photo.getUrl() != null) {
287 String type = photo.getType();
294 type = type.toLowerCase();
295 if (type.startsWith(
"image/")) {
296 type = type.substring(6);
298 String extension = photoTypeExtensions.get(type);
301 byte[] data = photo.getData();
302 String extractedFileName = String.format(
"photo_%d%s", i, extension == null ?
"" : extension);
303 String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
305 writeExtractedImage(extractedFilePath, data);
306 derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
307 abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
308 true, abstractFile, null, EmailParserModuleFactory.getModuleName(), null, null, TskData.EncodingType.NONE));
309 }
catch (IOException | TskCoreException ex) {
310 logger.log(Level.WARNING, String.format(
"Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex);
313 if (!derivedFilesCreated.isEmpty()) {
314 services.fireModuleContentEvent(
new ModuleContentEvent(abstractFile));
315 context.addFilesToJob(derivedFilesCreated);
319 logger.log(Level.INFO, String.format(
"Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
320 abstractFile.getName(), abstractFile.getId()));
322 }
catch (SecurityException ex) {
323 logger.log(Level.WARNING, String.format(
"Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
334 private void writeExtractedImage(String outputPath, byte[] data)
throws IOException {
335 File outputFile =
new File(outputPath);
336 FileOutputStream outputStream =
new FileOutputStream(outputFile);
337 outputStream.write(data);
348 private String getUniqueName(AbstractFile file) {
349 return file.getName() +
"_" + file.getId();
361 private String getFileRelativePath(String parentFileName, String fileName)
throws NoCurrentCaseException {
363 return Paths.get(getRelModuleOutputPath(), parentFileName, fileName).toString();
376 private String getOutputFolderPath(String parentFileName)
throws NoCurrentCaseException {
377 String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
378 File outputFilePath =
new File(outputFolderPath);
379 if (!outputFilePath.exists()) {
380 outputFilePath.mkdirs();
382 return outputFolderPath;
393 private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
394 String telephoneText = telephone.getText();
395 if (telephoneText == null || telephoneText.isEmpty()) {
400 List<TelephoneType> telephoneTypes = telephone.getTypes();
401 if (telephoneTypes.isEmpty()) {
402 ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
404 for (TelephoneType type : telephoneTypes) {
411 List<String> splitTelephoneTypes = Arrays.asList(
412 type.getValue().toUpperCase().replaceAll(
"\\s+",
"").split(
","));
414 for (String splitType : splitTelephoneTypes) {
415 String attributeTypeName =
"TSK_PHONE_" + splitType;
417 BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
418 if (attributeType == null) {
420 attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
421 BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
422 String.format(
"Phone (%s)", StringUtils.capitalize(splitType.toLowerCase())));
424 ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), attributeType, attributes);
425 }
catch (TskCoreException ex) {
426 logger.log(Level.SEVERE, String.format(
"Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
427 }
catch (TskDataException ex) {
428 logger.log(Level.SEVERE, String.format(
"Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
443 private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
444 String emailValue = email.getValue();
445 if (emailValue == null || emailValue.isEmpty()) {
450 List<EmailType> emailTypes = email.getTypes();
451 if (emailTypes.isEmpty()) {
452 ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
454 for (EmailType type : emailTypes) {
461 List<String> splitEmailTypes = Arrays.asList(
462 type.getValue().toUpperCase().replaceAll(
"\\s+",
"").split(
","));
464 for (String splitType : splitEmailTypes) {
465 String attributeTypeName =
"TSK_EMAIL_" + splitType;
467 BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
468 if (attributeType == null) {
470 attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
471 BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
472 String.format(
"Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
474 ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
475 }
catch (TskCoreException ex) {
476 logger.log(Level.SEVERE, String.format(
"Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
477 }
catch (TskDataException ex) {
478 logger.log(Level.SEVERE, String.format(
"Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
494 private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
495 String telephoneText = telephone.getText();
496 if (telephoneText == null || telephoneText.isEmpty()) {
502 AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
503 telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile);
504 accountInstances.add(phoneAccountInstance);
506 catch(TskCoreException ex) {
507 logger.log(Level.WARNING, String.format(
508 "Failed to create account for phone number '%s' (content='%s'; id=%d).",
509 telephoneText, abstractFile.getName(), abstractFile.getId()), ex);
522 private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
523 String emailValue = email.getValue();
524 if (emailValue == null || emailValue.isEmpty()) {
530 AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
531 emailValue, EmailParserModuleFactory.getModuleName(), abstractFile);
532 accountInstances.add(emailAccountInstance);
534 catch(TskCoreException ex) {
535 logger.log(Level.WARNING, String.format(
536 "Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
537 emailValue, abstractFile.getName(), abstractFile.getId()), ex);
548 private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
550 AccountFileInstance deviceAccountInstance = null;
551 String deviceId = null;
553 long dataSourceObjId = abstractFile.getDataSourceObjectId();
554 DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
555 deviceId = dataSource.getDeviceId();
556 deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
557 deviceId, EmailParserModuleFactory.getModuleName(), abstractFile);
559 catch (TskCoreException ex) {
560 logger.log(Level.WARNING, String.format(
561 "Failed to create device account for '%s' (content='%s'; id=%d).",
562 deviceId, abstractFile.getName(), abstractFile.getId()), ex);
564 catch (TskDataException ex) {
565 logger.log(Level.WARNING, String.format(
566 "Failed to get the data source from the case database (id=%d).",
567 abstractFile.getId()), ex);
570 return deviceAccountInstance;