diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..65d939f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "*test.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/package/bin/android-sms-extractor b/package/bin/android-sms-extractor index f1bf259..6e099ab 100755 --- a/package/bin/android-sms-extractor +++ b/package/bin/android-sms-extractor @@ -36,6 +36,14 @@ parser.add_argument('--datetime-format', action='store', dest='dt_format', help="Set datetime (strftime) format for text output (default: %(default)s)", default="%Y-%m-%d %H:%M:%S") +parser.add_argument('--destination', action='store', dest='destination', + help="Extract only conversations with this normalized destination" + ) + +parser.add_argument('--show-uris', action='store_true', dest='show_uris', + help="Append attachment URI to placeholder text when available" + ) + #=============================================================================== # Parse arguments #=============================================================================== @@ -58,9 +66,18 @@ c = db_conn.cursor() #=============================================================================== # Query and loop through all SMS conversations #=============================================================================== -c.execute("SELECT conversations._id, conversations.name, \ +query = "SELECT conversations._id, conversations.name, \ conversations.participant_normalized_destination \ - FROM conversations WHERE latest_message_id NOT NULL ORDER BY name") + FROM conversations WHERE latest_message_id NOT NULL" +params = [] + +if args.destination: + query += " AND conversations.participant_normalized_destination = ?" + params.append(args.destination) + +query += " ORDER BY name" + +c.execute(query, params) for conversation in c.fetchall(): data_id = conversation[0] @@ -71,29 +88,81 @@ for conversation in c.fetchall(): print("{} [{}]".format(data_name, data_dest)) print("==================================================") + # Detect available columns in the parts table for improved attachment handling + c.execute("PRAGMA table_info(parts)") + part_columns = [row[1] for row in c.fetchall()] + optional_part_columns = [ + "content_type", + "content_uri", + "display_name", + "name", + "filename", + ] + available_columns = ["parts.text", "parts.timestamp"] + [ + "parts." + name for name in optional_part_columns if name in part_columns + ] + select_columns = ", ".join(available_columns) + # Query and loop through all messages in this conversation - c.execute("SELECT messages._id, parts.text, parts.timestamp, \ - participants.sim_slot_id FROM messages \ - LEFT JOIN parts ON messages._id = parts.message_id \ - LEFT JOIN participants ON messages.sender_id = participants._id \ - WHERE messages.conversation_id = ? ORDER BY parts.timestamp", (data_id,)) + c.execute("SELECT messages._id, {} , participants.sim_slot_id FROM messages " + "LEFT JOIN parts ON messages._id = parts.message_id " \ + "LEFT JOIN participants ON messages.sender_id = participants._id " + "WHERE messages.conversation_id = ? ORDER BY parts.timestamp".format(select_columns), (data_id,)) for result in c.fetchall(): msg_id = result[0] msg_text = result[1] msg_time = result[2] - # Remove the last 3 digits (milliseconds) from timestamp - msg_time = int(str(msg_time)[:-3]) - msg_time_formatted = datetime.fromtimestamp(msg_time).strftime(args.dt_format) - - sim_slot_id = result[3] - - # A sim_slot_id equal to 0 indicates that the SMS was sent from the device - if sim_slot_id == 0: - msg_direction = "===>" + attachment_type = None + attachment_uri = None + attachment_name = None + attachment_filename = None + + if len(result) > 3: + attachment_type = result[3] + if len(result) > 4: + attachment_uri = result[4] + if len(result) > 5: + attachment_name = result[5] + if len(result) > 6: + attachment_filename = result[6] + + # Treat all-black block placeholders as absent text for media parts + if msg_text is not None: + trimmed = msg_text.strip() + if trimmed and all(ch in {"\u2588", "\u25A0", "\u2592", "\u2593"} for ch in trimmed): + msg_text = None + + # Remove the last 3 digits (milliseconds) from timestamp + msg_time = int(str(msg_time)[:-3]) + msg_time_formatted = datetime.fromtimestamp(msg_time).strftime(args.dt_format) + + if sim_slot_id == 0: + msg_direction = "===>" + else: + msg_direction = "<---" + + if msg_text is None or msg_text == "": + if attachment_type: + if attachment_type.startswith("image/"): + msg_text = "[Image attachment]" + elif attachment_type.startswith("audio/"): + msg_text = "[Audio attachment]" + elif attachment_type.startswith("video/"): + msg_text = "[Video attachment]" + else: + msg_text = "[{} attachment]".format(attachment_type) + if attachment_name: + msg_text += " {}".format(attachment_name) + elif attachment_filename: + msg_text += " {}".format(attachment_filename) + if args.show_uris and attachment_uri: + msg_text += " {}".format(attachment_uri) + elif not attachment_name and not attachment_filename and attachment_uri: + msg_text += " {}".format(attachment_uri) else: - msg_direction = "<---" + msg_text = "" - print("[{} {}]:\n{}\n".format( - msg_time_formatted, msg_direction, msg_text)) + print("[{} {}]:\n{}\n".format( + msg_time_formatted, msg_direction, msg_text)) diff --git a/readme.md b/readme.md index 6f1f3d1..0d4baaa 100644 --- a/readme.md +++ b/readme.md @@ -11,4 +11,7 @@ With this Python script you can extract all SMS messages from the SQLite databas 5. Pass the path to the extracted database file to this script: `$ android-sms-extractor bugle_db` +You can also append attachment information for media parts: +- `$ android-sms-extractor --show-uris bugle_db` + **IMPORTANT:** This script has been written in 2019 when I needed a quick solution to export all SMS from my Android device (*OnePlus X*) running *LineageOS 14.1*. This script was later in 2022 also successfully tested with the SMS database of `com.android.messaging` in *LineageOS 17.1*. If it also works for you: Good! If not: I'm sorry!