const plist = require('../../util/plist') const fileHash = require('../../util/backup_filehash') const log = require('../../util/log') const apple_timestamp = require('../../util/apple_timestamp') const SMS_DB = fileHash('Library/SMS/sms.db') module.exports = { version: 4, name: 'messages.conversations', description: `List all SMS and iMessage conversations`, requiresBackup: true, // Available fields. output: { id: el => el.ROWID, date: el => el.XFORMATTEDDATESTRING || '??', service: el => el.service_name + '', chatName: el => el.chat_identifier + '', displayName: el => el.display_name + '' }, // Run on a v3 lib / backup object. run (lib, { backup }) { return getConversations(backup) } } function getConversationsiOS9 (backup) { return new Promise((resolve, reject) => { backup.openDatabase(SMS_DB) .then(db => { db.all(`SELECT * FROM chat ORDER BY ROWID ASC`, async function (err, rows) { if (err) return reject(err) rows = rows || [] // We need to do some manual parsing of these records. // The timestamp information is stored in a binary blob named `properties` // Which is formatted as a binary PLIST. for (var el of rows) { if (el.properties) el.properties = plist.parseBuffer(el.properties) // Interestingly, some of these do not have dates attached. if (el.properties) { el.date = new Date(el.properties.CKChatWatermarkTime * 1000) } else { el.date = new Date(0) } // Format as YY-MM-DD HH:MM:SS try { el.XFORMATTEDDATESTRING = el.date.toISOString() .split('T') .join(' ') .split('Z') .join(' ') .split('.')[0] .trim() } catch (e) { el.XFORMATTEDDATESTRING = '' } } // Sort by the date. rows = rows.sort(function (a, b) { return (a.date.getTime() || 0) - (b.date.getTime() || 0) }) resolve(rows) }) }) .catch(reject) }) } function getConversationsiOS10iOS11 (backup) { return new Promise((resolve, reject) => { backup.openDatabase(SMS_DB) .then(db => { db.all(`SELECT *, ${apple_timestamp.parse('last_read_message_timestamp')} AS XFORMATTEDDATESTRING FROM chat ORDER BY last_read_message_timestamp ASC`, async function (err, rows) { if (err) return reject(err) rows = rows || [] resolve(rows) }) }) .catch(reject) }) } function getConversations (backup) { return new Promise(async (resolve, reject) => { try { let conversations = await getConversationsiOS10iOS11(backup) return resolve(conversations) } catch (e) { log.verbose('failed to read sms conversations as iOS10/11 format', e) } try { let conversations = await getConversationsiOS9(backup) return resolve(conversations) } catch (e) { log.verbose('failed to read sms conversations as iOS9 format', e) } reject(new Error('No suitable SMS database found. Use -v to see error informaton.')) }) }