├── .gitignore ├── LICENSE ├── README.md ├── imessage.py └── imessage_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matt Rajca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymessage-lite 2 | 3 | `pymessage-lite` is a simple Python library for fetching recipients and messages from OS X's Messages database. 4 | 5 | ## Background 6 | 7 | While there are no APIs for accessing iMessage data on OS X or iOS, OS X users can query the [sqlite](https://www.sqlite.org/) database that stores iMessage data. It can be found in the `~/Library/Messages` directory under the name `chat.db`. 8 | 9 | The database schema is undocumented, but by poking around with the `sqlite3` command line tool it's easy to figure out how it works. 10 | 11 | ## Exploring the Database 12 | 13 | Open a new Terminal window and enter: 14 | 15 | `sqlite3 ~/Library/Messages/chat.db` 16 | 17 | To list all the tables in the database, type `.tables` and hit Enter. The output should look something like: 18 | 19 | ``` 20 | _SqliteDatabaseProperties deleted_messages 21 | attachment handle 22 | chat message 23 | chat_handle_join message_attachment_join 24 | chat_message_join 25 | ``` 26 | 27 | The contents of these tables should be fairly self-explanatory. 28 | 29 | - `attachment` keeps track of any attachments (files, images, audio clips) sent or received, including paths to where they are stored locally as well as their file format. 30 | - `handle` keeps track of all known recipients (people with whom you previously exchanged iMessages). 31 | - `chat` keeps track of your conversation threads. 32 | - `message` keeps track of all messages along with their text contents, date, and the ID of the recipient. 33 | 34 | In my limited testing, I found `deleted_messages` is always empty. The other tables should not be necessary for most use cases. 35 | 36 | To view the full schema of a table and list all of its columns, type: 37 | 38 | `.schema ` 39 | 40 | Here is the output for `.schema message`: 41 | 42 | ``` 43 | CREATE TABLE message (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, guid TEXT UNIQUE NOT NULL, text TEXT, replace INTEGER DEFAULT 0, service_center TEXT, handle_id INTEGER DEFAULT 0, subject TEXT, country TEXT, attributedBody BLOB, version INTEGER DEFAULT 0, type INTEGER DEFAULT 0, service TEXT, account TEXT, account_guid TEXT, error INTEGER DEFAULT 0, date INTEGER, date_read INTEGER, date_delivered INTEGER, is_delivered INTEGER DEFAULT 0, is_finished INTEGER DEFAULT 0, is_emote INTEGER DEFAULT 0, is_from_me INTEGER DEFAULT 0, is_empty INTEGER DEFAULT 0, is_delayed INTEGER DEFAULT 0, is_auto_reply INTEGER DEFAULT 0, is_prepared INTEGER DEFAULT 0, is_read INTEGER DEFAULT 0, is_system_message INTEGER DEFAULT 0, is_sent INTEGER DEFAULT 0, has_dd_results INTEGER DEFAULT 0, is_service_message INTEGER DEFAULT 0, is_forward INTEGER DEFAULT 0, was_downgraded INTEGER DEFAULT 0, is_archive INTEGER DEFAULT 0, cache_has_attachments INTEGER DEFAULT 0, cache_roomnames TEXT, was_data_detected INTEGER DEFAULT 0, was_deduplicated INTEGER DEFAULT 0, is_audio_message INTEGER DEFAULT 0, is_played INTEGER DEFAULT 0, date_played INTEGER, item_type INTEGER DEFAULT 0, other_handle INTEGER DEFAULT -1, group_title TEXT, group_action_type INTEGER DEFAULT 0, share_status INTEGER, share_direction INTEGER, is_expirable INTEGER DEFAULT 0, expire_state INTEGER DEFAULT 0, message_action_type INTEGER DEFAULT 0, message_source INTEGER DEFAULT 0); 44 | ... 45 | ``` 46 | 47 | If you focus on the `CREATE_TABLE` line, you'll notice it's followed by the columns of the table. For example, the third column, `text TEXT`, stores the contents of the message as text and the `handle_id` column stores the ID of the recipient who sent the message, which you can in turn look up in the `handle` table. While none of this is documented, you can probably guess what each column stores. For example, `INTEGER is_read` probably stores a `1` if a message has been acknowledged with a read recipient and `0` otherwise. 48 | 49 | To view all your recipients, you can type: 50 | 51 | `SELECT * FROM handle;` 52 | 53 | And to view all message exchanged with a recipient of a given ID: 54 | 55 | `SELECT * FROM message WHERE handle_id=;` 56 | 57 | This is standard SQL, and there are plenty of [references on the web](http://www.w3schools.com/sql/sql_quickref.asp) detailing all the commands you can issue. 58 | 59 | **Warning:** Be careful! You can delete your entire iMessage history with commands such as `DELETE`. 60 | 61 | ## The Library 62 | 63 | The Python library simply uses the sqlite Python API to programmatically issue some of the commands you have seen above. 64 | 65 | The code for `get_all_recipients` simply connects to the sqlite3 database, issues a `SELECT` query to retrieve all recipients, and returns them as instances of a custom Python class, `Recipient`. 66 | 67 | If you've read everything above, this should be fairly straightforward: 68 | 69 | connection = _new_connection() 70 | c = connection.cursor() 71 | 72 | # The `handle` table stores all known recipients. 73 | c.execute("SELECT * FROM `handle`") 74 | recipients = [] 75 | for row in c: 76 | recipients.append(Recipient(row[0], row[1])) 77 | 78 | connection.close() 79 | return recipients 80 | 81 | Any command you enter in the `sqlite3` command line tool can be wrapped with a Python API as demonstrated above. 82 | 83 | ## Using the Library 84 | 85 | To try out the library, navigate to the `pymessage_lite` directory and enter `python imessage_test.py`. This test script will print out all your iMessage recipients and let you display all messages exchanged with a given recipient. 86 | 87 | ## Compatibility 88 | 89 | This project has been written on and tested with OS X 10.11 "El Capitan". It might stop working in future releases of OS X as iMessage evolves. 90 | -------------------------------------------------------------------------------- /imessage.py: -------------------------------------------------------------------------------- 1 | from os.path import expanduser 2 | import sqlite3 3 | import datetime 4 | 5 | OSX_EPOCH = 978307200 6 | 7 | # Represents a user that iMessages can be exchanged with. 8 | # 9 | # Each user has... 10 | # - an `id` property that uniquely identifies him or her in the Messages database 11 | # - a `phone_or_email` property that is either the user's phone number or iMessage-enabled email address 12 | class Recipient: 13 | def __init__(self, id, phone_or_email): 14 | self.id = id 15 | self.phone_or_email = phone_or_email 16 | 17 | def __repr__(self): 18 | return "ID: " + str(self.id) + " Phone or email: " + self.phone_or_email 19 | 20 | # Represents an iMessage message. 21 | # 22 | # Each message has: 23 | # - a `text` property that holds the text contents of the message 24 | # - a `date` property that holds the delivery date of the message 25 | class Message: 26 | def __init__(self, text, date): 27 | self.text = text 28 | self.date = date 29 | 30 | def __repr__(self): 31 | return "Text: " + self.text + " Date: " + str(self.date) 32 | 33 | def _new_connection(): 34 | # The current logged-in user's Messages sqlite database is found at: 35 | # ~/Library/Messages/chat.db 36 | db_path = expanduser("~") + '/Library/Messages/chat.db' 37 | return sqlite3.connect(db_path) 38 | 39 | # Fetches all known recipients. 40 | # 41 | # The `id`s of the recipients fetched can be used to fetch all messages exchanged with a given recipient. 42 | def get_all_recipients(): 43 | connection = _new_connection() 44 | c = connection.cursor() 45 | 46 | # The `handle` table stores all known recipients. 47 | c.execute("SELECT * FROM `handle`") 48 | recipients = [] 49 | for row in c: 50 | recipients.append(Recipient(row[0], row[1])) 51 | 52 | connection.close() 53 | return recipients 54 | 55 | # Fetches all messages exchanged with a given recipient. 56 | def get_messages_for_recipient(id): 57 | connection = _new_connection() 58 | c = connection.cursor() 59 | 60 | # The `message` table stores all exchanged iMessages. 61 | c.execute("SELECT * FROM `message` WHERE handle_id=" + str(id)) 62 | messages = [] 63 | for row in c: 64 | text = row[2] 65 | if text is None: 66 | continue 67 | date = datetime.datetime.fromtimestamp(row[15] + OSX_EPOCH) 68 | 69 | # Strip any special non-ASCII characters (e.g., the special character that is used as a placeholder for attachments such as files or images). 70 | encoded_text = text.encode('ascii', 'ignore') 71 | messages.append(Message(encoded_text, date)) 72 | connection.close() 73 | return messages 74 | -------------------------------------------------------------------------------- /imessage_test.py: -------------------------------------------------------------------------------- 1 | import imessage 2 | 3 | raw_input("Hello, press any key to print all known recipients you exchanged iMessages with.") 4 | print(imessage.get_all_recipients()) 5 | 6 | recipient_id_str = raw_input("Enter the ID (number) of the recipient you wish to view message for:") 7 | 8 | print(imessage.get_messages_for_recipient(int(recipient_id_str))) 9 | --------------------------------------------------------------------------------