Skip to content

Instantly share code, notes, and snippets.

@cemiu
Last active December 23, 2022 23:14
Show Gist options
  • Save cemiu/eef4d656cdd2c27f656af8ff400e03d1 to your computer and use it in GitHub Desktop.
Save cemiu/eef4d656cdd2c27f656af8ff400e03d1 to your computer and use it in GitHub Desktop.

Revisions

  1. cemiu revised this gist Dec 23, 2022. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions imessage_text_exporter_macos.py
    Original file line number Diff line number Diff line change
    @@ -7,9 +7,8 @@
    # Create a local, unencrypted (!) backup of an iOS / iPadOS device, through Finder
    # Open System Settings > Security & Privacy > Privacy > Full Disk Access > Add Terminal.app (Utilities folder)
    # Download imessage_text_exporter_macos.py file, run Terminal, execute:
    # python imessage_text_exporter_macos.py > imessage.txt
    # python imessage_text_exporter_macos.py
    # Find conversation by searching messages contained within
    # Exported conversation should be in: imessage.txt

    # If you want the names in the conversations to be different, change the name_self and name_other variables below

  2. cemiu created this gist Dec 23, 2022.
    144 changes: 144 additions & 0 deletions imessage_text_exporter_macos.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,144 @@
    # Written by cemiu, distributed under the MIT License

    # Quick and dirty script to export iMessage conversations as searchable text; Without attatchments or images.
    # This version supports macOS only, though Windows support should be possible with some light modifications.

    # Usage:
    # Create a local, unencrypted (!) backup of an iOS / iPadOS device, through Finder
    # Open System Settings > Security & Privacy > Privacy > Full Disk Access > Add Terminal.app (Utilities folder)
    # Download imessage_text_exporter_macos.py file, run Terminal, execute:
    # python imessage_text_exporter_macos.py > imessage.txt
    # Find conversation by searching messages contained within
    # Exported conversation should be in: imessage.txt

    # If you want the names in the conversations to be different, change the name_self and name_other variables below

    # Grouping by replies is not supported
    # Group chats are not supported, though support can be added with a little motivation
    # Windows is not supported, though support can be added with a little motivation

    import sqlite3
    import sys
    import os
    import time
    from contextlib import closing

    db_file = None
    handle_id = None
    name_self = 'Me'
    name_other = 'Other'

    mac_path = os.path.expanduser('~/Library/Messages/chat.db')
    ios_path = os.path.expanduser('~/Library/Application Support/MobileSync/Backup/')
    obj_rep = u'\ufffc'

    print_warning = lambda text: print(f'\033[93m{text}\033[0m')


    def load_ios_backup_path():
    """Find the most recent iOS backup and return the path to the Messages DB."""
    try:
    recent_backup, recent_backup_date = None, 0
    for folder in os.listdir(ios_path):
    db_path = os.path.join(ios_path, folder, '3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28')
    if os.path.exists(db_path):
    folder_time = os.path.getctime(os.path.join(ios_path, folder))
    if folder_time > recent_backup_date:
    recent_backup_date = folder_time
    recent_backup = db_path

    if recent_backup is None:
    print_warning('No iOS backup found. Please connect your iPhone and run create a local backup.')
    sys.exit(1)

    backup_recency = (time.time() - recent_backup_date) / 60 / 60 / 24
    print(f'Using iOS backup that is {backup_recency:.0f} days old.')
    if backup_recency > 7:
    print_warning('It is recommended to create a fresh backup.')

    return recent_backup
    except PermissionError:
    print_warning('Permission to directory denied!\nTo fix: '
    'System Settings > Security & Privacy > Privacy > Full Disk Access > Add Terminal.app')
    print('\nOr copy following file to a different location, and rerun with custom path:\n'
    '~/Library/Application Support/MobileSync/Backup/<backup-id>/3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28')
    sys.exit(1)


    def select_db():
    """User selects which DB to use."""
    if len(sys.argv) == 2:
    print(f'Using custom DB path: {sys.argv[1]}')
    return sys.argv[1]
    elif len(sys.argv) < 2:
    print('1: iOS Backup (must be unencrypted)\n2: Custom path\n')
    choice = input('Enter your choice: ')
    if choice == '1': return load_ios_backup_path()
    elif choice == '2': return input('Enter custom path: ')
    else: print('Invalid option. Exiting.'); sys.exit(1)
    print_warning('Supply either no arguments or a single argument for the DB path. Current arguments:')
    print_warning('\n'.join(sys.argv[1:]))
    sys.exit(1)


    def db_con(db):
    if not os.path.exists(db):
    print_warning(f'Path does not exist: {db}')
    sys.exit(1)

    try:
    return sqlite3.connect(db)
    except sqlite3.OperationalError:
    print(f'Could not open database file: {db}')
    sys.exit(1)


    def get_handle_id(c, handle):
    while handle is None:
    message_match = input('Find a conversation by searching for it.\n'
    'Capitalisation, spacing, etc are important!:\n')

    c.execute('SELECT handle_id, text FROM message WHERE text LIKE ?', (f'%{message_match}%',))
    rows = c.fetchall()
    if len(rows) == 0: print_warning(u'\nNo conversation found, try again or quit using \u2303+C.\n'); continue
    if len(rows) > 1:
    for i, row in enumerate(rows): print(f'{i}: {row[1]}')
    print_warning(f'\nFound {len(rows)} conversations, please be more specific or quit using \u2303+C.\n')
    continue

    handle = rows[0][0]
    return handle


    def main():
    global db_file, handle_id, name_self, name_other
    if db_file is None: db_file = select_db()

    with db_con(db_file) as con, closing(con.cursor()) as c1, closing(con.cursor()) as c2:
    handle_id = get_handle_id(c1, handle_id)

    c1.execute(f'''
    SELECT ROWID, text, is_from_me
    FROM message
    WHERE (handle_id IS {handle_id} AND cache_roomnames IS NULL)
    ''')

    last_sender = None
    for row in c1:
    if row[2] != last_sender:
    last_sender = row[2]
    print(f'\n{name_self}:' if last_sender == 1 else f'\n{name_other}:')

    c2.execute('SELECT * FROM message_attachment_join WHERE message_id IS ?', (row[0],))
    attachment = c2.fetchone()
    if attachment:
    c2.execute('SELECT mime_type FROM attachment WHERE ROWID IS ?', (attachment[1],))
    mime_type = c2.fetchone()[0]
    print(f'\t<Attachment: {mime_type}>' if mime_type else '\t<Other attachment>')

    if row[1]:
    print(f'\t{row[1].strip().replace(obj_rep, "")}')


    if __name__ == '__main__':
    main()