diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java index be85936754..f5d728b633 100644 --- a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java @@ -64,6 +64,8 @@ final class AddLogicalImageTask implements Runnable { private final static String MODULE_NAME = "Logical Imager"; //NON-NLS private final static String ROOT_STR = "root"; // NON-NLS private final static String VHD_EXTENSION = ".vhd"; // NON-NLS + private final static int REPORT_PROGRESS_INTERVAL = 100; + private final static int POST_ARTIFACT_INTERVAL = 1000; private final String deviceId; private final String timeZone; private final File src; @@ -145,7 +147,7 @@ final class AddLogicalImageTask implements Runnable { callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); return; } - + // Add the SearchResults.txt and users.txt to the case report String resultsFilename; if (Paths.get(dest.toString(), SEARCH_RESULTS_TXT).toFile().exists()) { @@ -200,7 +202,7 @@ final class AddLogicalImageTask implements Runnable { List newDataSources = new ArrayList<>(); Map> interestingFileMap = new HashMap<>(); - + if (imagePaths.isEmpty()) { createVHD = false; // No VHD in src directory, try ingest the root directory as local files @@ -241,7 +243,7 @@ final class AddLogicalImageTask implements Runnable { if (addMultipleImagesTask.getResult() == DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS) { LOGGER.log(Level.SEVERE, "Failed to add VHD datasource"); // NON-NLS callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, addMultipleImagesTask.getErrorMessages(), emptyDataSources); - return; + return; } try { interestingFileMap = getInterestingFileMapForVHD(Paths.get(dest.toString(), resultsFilename)); @@ -250,7 +252,7 @@ final class AddLogicalImageTask implements Runnable { LOGGER.log(Level.SEVERE, "Failed to add interesting files", ex); // NON-NLS callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.NONCRITICAL_ERRORS, errorList, emptyDataSources); } - + } catch (NoCurrentCaseException ex) { String msg = Bundle.AddLogicalImageTask_noCurrentCase(); errorList.add(msg); @@ -261,7 +263,7 @@ final class AddLogicalImageTask implements Runnable { if (cancelled) { if (!createVHD) { - // TODO: When 5453 is fixed, we should be able to delete it when adding VHD. + // TODO: When 5453 is fixed, we should be able to delete it when adding VHD. deleteDestinationDirectory(); } errorList.add(Bundle.AddLogicalImageTask_addImageCancelled()); @@ -345,7 +347,7 @@ final class AddLogicalImageTask implements Runnable { private void addInterestingFiles(Map> interestingFileMap) throws IOException, TskCoreException { int lineNumber = 0; List artifacts = new ArrayList<>(); - + Iterator>> iterator = interestingFileMap.entrySet().iterator(); while (iterator.hasNext()) { @@ -365,21 +367,23 @@ final class AddLogicalImageTask implements Runnable { List fileIds = entry.getValue(); for (Long fileId: fileIds) { - if (lineNumber % 100 == 0) { + if (cancelled) { + postArtifacts(artifacts); + return; + } + if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingInterestingFile(lineNumber, totalFiles)); } + if (lineNumber % POST_ARTIFACT_INTERVAL == 0) { + postArtifacts(artifacts); + artifacts.clear(); + } addInterestingFileToArtifacts(fileId, ruleSetName, ruleName, artifacts); lineNumber++; } iterator.remove(); } - - try { - // index the artifact for keyword search - blackboard.postArtifacts(artifacts, MODULE_NAME); - } catch (Blackboard.BlackboardException ex) { - LOGGER.log(Level.SEVERE, "Unable to post artifacts to blackboard", ex); //NON-NLS - } + postArtifacts(artifacts); } private void addInterestingFileToArtifacts(long fileId, String ruleSetName, String ruleName, List artifacts) throws TskCoreException { @@ -400,7 +404,7 @@ final class AddLogicalImageTask implements Runnable { Map> objIdToimagePathsMap = currentCase.getSleuthkitCase().getImagePaths(); imagePathToObjIdMap = imagePathsToDataSourceObjId(objIdToimagePathsMap); Map> interestingFileMap = new HashMap<>(); - + try (BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream(resultsPath.toFile()), "UTF8"))) { // NON-NLS String line; @@ -426,7 +430,7 @@ final class AddLogicalImageTask implements Runnable { String filename = fields[7]; String parentPath = fields[8]; - if (lineNumber % 100 == 0) { + if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_searchingInterestingFile(lineNumber, totalFiles)); } @@ -443,11 +447,20 @@ final class AddLogicalImageTask implements Runnable { interestingFileMap.put(key, fileIds); } lineNumber++; - } // end reading file } + } // end reading file } return interestingFileMap; } + private void postArtifacts(List artifacts) { + try { + // index the artifact for keyword search + blackboard.postArtifacts(artifacts, MODULE_NAME); + } catch (Blackboard.BlackboardException ex) { + LOGGER.log(Level.SEVERE, "Unable to post artifacts to blackboard", ex); //NON-NLS + } + } + @Messages({ "# {0} - file number", "# {1} - total files", "AddLogicalImageTask.addingExtractedFile=Adding extracted files ({0}/{1})" }) @@ -455,7 +468,7 @@ final class AddLogicalImageTask implements Runnable { SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase(); SleuthkitCase.CaseDbTransaction trans = null; Map> interestingFileMap = new HashMap<>(); - + try { trans = skCase.beginTransaction(); LocalFilesDataSource localFilesDataSource = skCase.addLocalFilesDataSource(deviceId, this.src.getName(), timeZone, trans); @@ -492,7 +505,7 @@ final class AddLogicalImageTask implements Runnable { String ctime = fields[13]; parentPath = ROOT_STR + "/" + vhdFilename + "/" + parentPath; - if (lineNumber % 100 == 0) { + if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingExtractedFile(lineNumber, totalFiles)); } diff --git a/InternalPythonModules/android/imo.py b/InternalPythonModules/android/imo.py index 5a653ff9d5..714c029445 100644 --- a/InternalPythonModules/android/imo.py +++ b/InternalPythonModules/android/imo.py @@ -29,6 +29,7 @@ from java.util.logging import Level from java.util import ArrayList from org.apache.commons.codec.binary import Base64 from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.autopsy.coreutils import Logger from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil from org.sleuthkit.autopsy.coreutils import AppSQLiteDB @@ -56,10 +57,14 @@ and adds artifacts to the case. class IMOAnalyzer(general.AndroidComponentAnalyzer): def __init__(self): self._logger = Logger.getLogger(self.__class__.__name__) + self._PACKAGE_NAME = "com.imo.android.imous" + self._PARSER_NAME = "IMO Parser" + self._MESSAGE_TYPE = "IMO Message" + self._VERSION = "9.8.0" def analyze(self, dataSource, fileManager, context): selfAccountAddress = None - accountDbs = AppSQLiteDB.findAppDatabases(dataSource, "accountdb.db", True, "com.imo.android.imous") + accountDbs = AppSQLiteDB.findAppDatabases(dataSource, "accountdb.db", True, self._PACKAGE_NAME) for accountDb in accountDbs: try: accountResultSet = accountDb.runQuery("SELECT uid, name FROM account") @@ -71,16 +76,26 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): selfAccountAddress = Account.Address(accountResultSet.getString("uid"), accountResultSet.getString("name")) except SQLException as ex: - self._logger.log(Level.SEVERE, "Error processing query result for account", ex) + self._logger.log(Level.WARNING, "Error processing query result for account", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) finally: accountDb.close() - friendsDbs = AppSQLiteDB.findAppDatabases(dataSource, "imofriends.db", True, "com.imo.android.imous") + friendsDbs = AppSQLiteDB.findAppDatabases(dataSource, "imofriends.db", True, self._PACKAGE_NAME) for friendsDb in friendsDbs: try: - friendsDBHelper = CommunicationArtifactsHelper(Case.getCurrentCase().getSleuthkitCase(), - "IMO Parser", friendsDb.getDBFile(), + current_case = Case.getCurrentCaseThrows() + if selfAccountAddress is not None: + friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(), + self._PARSER_NAME, + friendsDb.getDBFile(), Account.Type.IMO, Account.Type.IMO, selfAccountAddress ) + else: + friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(), + self._PARSER_NAME, + friendsDb.getDBFile(), + Account.Type.IMO + ) contactsResultSet = friendsDb.runQuery("SELECT buid, name FROM friends") if contactsResultSet is not None: while contactsResultSet.next(): @@ -121,7 +136,7 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): messageArtifact = friendsDBHelper.addMessage( - "IMO Message", + self._MESSAGE_TYPE, direction, fromAddress, toAddress, @@ -137,8 +152,16 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): except SQLException as ex: self._logger.log(Level.WARNING, "Error processing query result for IMO friends", ex) - except (TskCoreException, BlackboardException) as ex: - self._logger.log(Level.WARNING, "Failed to message artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, "Failed to add IMO message artifacts.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, "Failed to post artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except NoCurrentCaseException as ex: + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) finally: friendsDb.close() diff --git a/InternalPythonModules/android/module.py b/InternalPythonModules/android/module.py index 9893df2b74..5152d1288b 100644 --- a/InternalPythonModules/android/module.py +++ b/InternalPythonModules/android/module.py @@ -48,6 +48,7 @@ import textmessage import wwfmessage import imo import line +import textnow class AndroidModuleFactory(IngestModuleFactoryAdapter): @@ -92,7 +93,7 @@ class AndroidIngestModule(DataSourceIngestModule): analyzers = [contact.ContactAnalyzer(), calllog.CallLogAnalyzer(), textmessage.TextMessageAnalyzer(), tangomessage.TangoMessageAnalyzer(), wwfmessage.WWFMessageAnalyzer(), googlemaplocation.GoogleMapLocationAnalyzer(), browserlocation.BrowserLocationAnalyzer(), - cachelocation.CacheLocationAnalyzer(), imo.IMOAnalyzer(), line.LineAnalyzer()] + cachelocation.CacheLocationAnalyzer(), imo.IMOAnalyzer(), line.LineAnalyzer(), textnow.TextNowAnalyzer()] self.log(Level.INFO, "running " + str(len(analyzers)) + " analyzers") progressBar.switchToDeterminate(len(analyzers)) diff --git a/InternalPythonModules/android/textnow.py b/InternalPythonModules/android/textnow.py new file mode 100644 index 0000000000..216864f2e1 --- /dev/null +++ b/InternalPythonModules/android/textnow.py @@ -0,0 +1,392 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from java.io import File +from java.lang import Class +from java.lang import ClassNotFoundException +from java.lang import Long +from java.lang import String +from java.sql import ResultSet +from java.sql import SQLException +from java.sql import Statement +from java.util.logging import Level +from java.util import ArrayList +from org.apache.commons.codec.binary import Base64 +from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.coreutils import Logger +from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil +from org.sleuthkit.autopsy.coreutils import AppSQLiteDB + +from org.sleuthkit.autopsy.datamodel import ContentUtils +from org.sleuthkit.autopsy.ingest import IngestJobContext +from org.sleuthkit.datamodel import AbstractFile +from org.sleuthkit.datamodel import BlackboardArtifact +from org.sleuthkit.datamodel import BlackboardAttribute +from org.sleuthkit.datamodel import Content +from org.sleuthkit.datamodel import TskCoreException +from org.sleuthkit.datamodel.Blackboard import BlackboardException +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection + +from TskMessagesParser import TskMessagesParser +from TskContactsParser import TskContactsParser +from TskCallLogsParser import TskCallLogsParser + +import traceback +import general + +class TextNowAnalyzer(general.AndroidComponentAnalyzer): + """ + Parses the TextNow App databases for TSK contacts, message + and calllog artifacts. + + The TextNow database in v6.41.0.2 is structured as follows: + - A messages table, which stores messages from/to a number + - A contacts table, which stores phone numbers + - A groups table, which stores each group the device owner is a part of + - A group_members table, which stores who is in each group + + The messages table contains both call logs and messages, with a type + column differentiating the two. + """ + + def __init__(self): + self._logger = Logger.getLogger(self.__class__.__name__) + self._TEXTNOW_PACKAGE_NAME = "com.enflick.android.TextNow" + self._PARSER_NAME = "TextNow Parser" + self._VERSION = "6.41.0.2" + + def analyze(self, dataSource, fileManager, context): + """ + Extract, Transform and Load all messages, contacts and + calllogs from the TextNow databases. + """ + + textnow_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "textnow_data.db", True, self._TEXTNOW_PACKAGE_NAME) + + try: + for textnow_db in textnow_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + textnow_db.getDBFile(), Account.Type.TEXTNOW + ) + self.parse_contacts(textnow_db, helper) + self.parse_calllogs(textnow_db, helper) + self.parse_messages(textnow_db, helper) + except NoCurrentCaseException as ex: + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + for textnow_db in textnow_dbs: + textnow_db.close() + + def parse_contacts(self, textnow_db, helper): + #Query for contacts and iterate row by row adding + #each contact artifact + try: + contacts_parser = TextNowContactsParser(textnow_db) + while contacts_parser.next(): + helper.addContact( + contacts_parser.get_account_name(), + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + contacts_parser.close() + except SQLException as ex: + #Error parsing TextNow db + self._logger.log(Level.WARNING, "Error parsing TextNow databases for contacts", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifacts to the case database.. case database is not complete. + self._logger.log(Level.SEVERE, + "Error adding TextNow contacts artifacts to the case database", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard... + self._logger.log(Level.WARNING, + "Error posting TextNow contacts artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_calllogs(self, textnow_db, helper): + #Query for call logs and iterate row by row adding + #each call log artifact + try: + calllog_parser = TextNowCallLogsParser(textnow_db) + while calllog_parser.next(): + helper.addCalllog( + calllog_parser.get_call_direction(), + calllog_parser.get_phone_number_from(), + calllog_parser.get_phone_number_to(), + calllog_parser.get_call_start_date_time(), + calllog_parser.get_call_end_date_time(), + calllog_parser.get_call_type() + ) + calllog_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error parsing TextNow databases for calllogs", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifacts to the case database.. case database is not complete. + self._logger.log(Level.SEVERE, + "Error adding TextNow call log artifacts to the case database", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard... + self._logger.log(Level.WARNING, + "Error posting TextNow call log artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_messages(self, textnow_db, helper): + #Query for messages and iterate row by row adding + #each message artifact + try: + messages_parser = TextNowMessagesParser(textnow_db) + while messages_parser.next(): + helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + messages_parser.close() + except SQLException as ex: + #Error parsing TextNow db + self._logger.log(Level.WARNING, "Error parsing TextNow databases for messages.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifacts to the case database.. case database is not complete. + self._logger.log(Level.SEVERE, + "Error adding TextNow messages artifacts to the case database", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard... + self._logger.log(Level.WARNING, + "Error posting TextNow messages artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + +class TextNowCallLogsParser(TskCallLogsParser): + """ + Extracts TSK_CALLLOG information from the TextNow database. + TSK_CALLLOG fields that are not in the TextNow database are given + a default value inherited from the super class. + """ + + def __init__(self, calllog_db): + """ + message_type of 100 or 102 are for calls (audio, video) + """ + super(TextNowCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT contact_value AS num, + message_direction AS direction, + message_text AS duration, + date AS datetime + FROM messages AS M + WHERE message_type IN ( 100, 102 ) + """ + ) + ) + self._INCOMING_CALL_TYPE = 1 + self._OUTGOING_CALL_TYPE = 2 + + def get_phone_number_from(self): + if self.get_call_direction() == self.OUTGOING_CALL: + return super(TextNowCallLogsParser, self).get_phone_number_from() + return Account.Address(self.result_set.getString("num"), + self.result_set.getString("num")) + + def get_phone_number_to(self): + if self.get_call_direction() == self.INCOMING_CALL: + return super(TextNowCallLogsParser, self).get_phone_number_to() + return Account.Address(self.result_set.getString("num"), + self.result_set.getString("num")) + + def get_call_direction(self): + if self.result_set.getInt("direction") == self._INCOMING_CALL_TYPE: + return self.INCOMING_CALL + return self.OUTGOING_CALL + + def get_call_start_date_time(self): + return self.result_set.getLong("datetime") / 1000 + + def get_call_end_date_time(self): + start = self.get_call_start_date_time() + duration = self.result_set.getString("duration") + try: + return start + long(duration) + except ValueError as ve: + return super(TextNowCallLogsParser, self).get_call_end_date_time() + +class TextNowContactsParser(TskContactsParser): + """ + Extracts TSK_CONTACT information from the TextNow database. + TSK_CONTACT fields that are not in the TextNow database are given + a default value inherited from the super class. + """ + + def __init__(self, contact_db): + super(TextNowContactsParser, self).__init__(contact_db.runQuery( + """ + SELECT C.contact_value AS number, + CASE + WHEN contact_name IS NULL THEN contact_value + WHEN contact_name == "" THEN contact_value + ELSE contact_name + END name + FROM contacts AS C + """ + ) + ) + + def get_account_name(self): + return self.result_set.getString("number") + + def get_contact_name(self): + return self.result_set.getString("name") + + def get_phone(self): + return self.result_set.getString("number") + +class TextNowMessagesParser(TskMessagesParser): + """ + Extract TSK_MESSAGE information from the TextNow database. + TSK_CONTACT fields that are not in the TextNow database are given + a default value inherited from the super class. + """ + + def __init__(self, message_db): + """ + The query below does the following: + - The group_info inner query creates a comma seperated list of group recipients + for each group. This result is then joined on the groups table to get the thread id. + - The contacts table is unioned with this result so we have a complete map + of "from" phone_numbers -> recipients (group or single). This is the + 'to_from_map' inner query. + - Finally, the to_from_map results are joined with the messages table to get all + of the communication details. + """ + super(TextNowMessagesParser, self).__init__(message_db.runQuery( + """ + + SELECT CASE + WHEN message_direction == 2 THEN "" + WHEN to_addresses IS NULL THEN M.contact_value + ELSE contact_name + end from_address, + CASE + WHEN message_direction == 1 THEN "" + WHEN to_addresses IS NULL THEN M.contact_value + ELSE to_addresses + end to_address, + message_direction, + message_text, + M.READ, + M.date, + M.attach, + thread_id + FROM (SELECT group_info.contact_value, + group_info.to_addresses, + G.contact_value AS thread_id + FROM (SELECT GM.contact_value, + Group_concat(GM.member_contact_value) AS to_addresses + FROM group_members AS GM + GROUP BY GM.contact_value) AS group_info + JOIN groups AS G + ON G.contact_value = group_info.contact_value + UNION + SELECT c.contact_value, + NULL, + "-1" + FROM contacts AS c) AS to_from_map + JOIN messages AS M + ON M.contact_value = to_from_map.contact_value + WHERE message_type NOT IN ( 102, 100 ) + """ + ) + ) + self._TEXTNOW_MESSAGE_TYPE = "TextNow Message" + self._INCOMING_MESSAGE_TYPE = 1 + self._OUTGOING_MESSAGE_TYPE = 2 + self._UNKNOWN_THREAD_ID = "-1" + + def get_message_type(self): + return self._TEXTNOW_MESSAGE_TYPE + + def get_phone_number_from(self): + if self.result_set.getString("from_address") == "": + return super(TextNowMessagesParser, self).get_phone_number_from() + return Account.Address(self.result_set.getString("from_address"), + self.result_set.getString("from_address")) + + def get_message_direction(self): + direction = self.result_set.getInt("message_direction") + if direction == self._INCOMING_MESSAGE_TYPE: + return self.INCOMING + return self.OUTGOING + + def get_phone_number_to(self): + if self.result_set.getString("to_address") == "": + return super(TextNowMessagesParser, self).get_phone_number_to() + recipients = self.result_set.getString("to_address").split(",") + + recipient_accounts = [] + for recipient in recipients: + recipient_accounts.append(Account.Address(recipient, recipient)) + + return recipient_accounts + + def get_message_date_time(self): + #convert ms to s + return self.result_set.getLong("date") / 1000; + + def get_message_read_status(self): + read = self.result_set.getBoolean("read") + if self.get_message_direction() == self.INCOMING: + if read == True: + return self.READ + return self.UNREAD + + #read status for outgoing messages cannot be determined, give default + return super(TextNowMessagesParser, self).get_message_read_status() + + def get_message_text(self): + text = self.result_set.getString("message_text") + attachment = self.result_set.getString("attach") + if attachment != "": + text = general.appendAttachmentList(text, [attachment]) + return text + + def get_thread_id(self): + thread_id = self.result_set.getString("thread_id") + if thread_id == self._UNKNOWN_THREAD_ID: + return super(TextNowMessagesParser, self).get_thread_id() + return thread_id