How to convert iChat message backups (.ichat, Apple Binary Property List) using Python

First, download this tool:

wget https://raw.githubusercontent.com/cclgroupltd/ccl-bplist/master/ccl_bplist.py

Now create bplist2json.py which is based on this old gist by Benno Kruit

#!/usr/bin/env python3
"""
Convert an Apple Binary Property List (bplist) to json
"""

import ccl_bplist # https://github.com/cclgroupltd/ccl-bplist

from datetime import datetime

def clean_archive(d):
    if type(d) in [dict, ccl_bplist.NsKeyedArchiverDictionary]:
        return {k:clean_archive(v) for k,v in d.items() if not k.startswith('$')}
    elif type(d) == ccl_bplist.NsKeyedArchiverList:
        return [clean_archive(i) for i in d]
    else:
        return d

def bplist_dict(fobj):
    """Convert a bplist file object to python dict"""
    plist = ccl_bplist.load(fobj)
    ccl_bplist.set_object_converter(ccl_bplist.NSKeyedArchiver_common_objects_convertor)
    archive = ccl_bplist.deserialise_NsKeyedArchiver(plist)
    return clean_archive(archive)


if __name__ == '__main__':
    import argparse, sys, json
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--input', '-i', nargs='?', type=argparse.FileType('rb'),
        default=sys.stdin, help='default: stdin')
    args = parser.parse_args()

    class ExportEncoder(json.JSONEncoder):
        def default(self, o):
            if isinstance(o, datetime):
                return o.isoformat()
            if isinstance(o, bytes):
                return o.decode('iso-8859-1')
            return json.JSONEncoder.default(self, o)
    
    print(json.dumps(bplist_dict(args.input), cls=ExportEncoder, indent=4))

Now create my script extract-chatlog.py which converts the JSON files to human readable chat logs

#!/usr/bin/env python3
import json
import sys
from datetime import datetime

# Function to format datetime string
def format_datetime(dt_str):
    dt = datetime.fromisoformat(dt_str)
    return dt.strftime("%Y-%m-%d %H:%M:%S")

def main(input_file):
    # Read JSON data from input file
    with open(input_file, 'r', encoding='utf-8') as f:
        data = json.load(f)

    # Extract the messages list
    messages = data[2]

    # Extract chat log in a human-readable format
    chat_log = []
    for message in messages:
        sender = message['Sender']['ID']
        time = format_datetime(message['Time'])
        text = message['MessageText']['NSString']
        chat_log.append(f"{time} - {sender}: {text}")

    # Print chat log to stdout
    for log in chat_log:
        print(log)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python script.py <input_file>")
        sys.exit(1)

    input_file = sys.argv[1]
    main(input_file)

Now it’s time to run it. Generally, run bplist2json.py on the .ichat to obtain a JSON file and then run extract-chatlog.py to obtain a text conversation log with timestamps etc.

Here’s a bash oneliner to run it on all *.ichat files in the current directory

for i in *.ichat ; do ./bplist2json.py -i ${i} > "${i}.json" ; ./extract-chatlog.py "${i}.json" > ${i}.txt ; done