├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── dev.md ├── examples └── flask-app.py ├── py_imessage ├── __init__.py ├── db_conn.py ├── imessage.py └── osascript │ ├── check_imessage.js │ └── send_message.js ├── requirements.txt └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: rolstenhouse 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_STORE 3 | env 4 | .git 5 | .pyc 6 | __pycache__ 7 | build 8 | dist 9 | py_iMessage.egg-info/ 10 | test.py -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rob Olsthoorn 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include py_imessage *.js -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | No longer under active development 2 | 3 | 4 | =========== 5 | py-imessage 6 | =========== 7 | |License| |Downloads| 8 | 9 | py-imessage is a library to send iMessages from your Mac computer (it does not work on Windows/Linux). It was originally used to build an API for iMessages; however, Apple doesn't support third-parties using iMessage over a few hundred marketing messages per day. 10 | 11 | 12 | .. raw:: html 13 | 14 | Buy Me A Coffee 15 | 16 | ------------ 17 | Installation 18 | ------------ 19 | 20 | Run the following commands on the terminal 21 | 22 | .. code:: bash 23 | 24 | pip install py-imessage 25 | 26 | # Disable system integrity protection in order to allow access to chat.db 27 | csrutil disable 28 | 29 | If running :code:`csrutil disable` doesn't work. Try `this stackoverflow post `_ 30 | 31 | ------------ 32 | Sample Usage 33 | ------------ 34 | 35 | .. code:: python 36 | 37 | from py_imessage import imessage 38 | import sleep 39 | 40 | phone = "1234567890" 41 | 42 | if not imessage.check_compatibility(phone): 43 | print("Not an iPhone") 44 | 45 | guid = imessage.send(phone, "Hello World!") 46 | 47 | # Let the recipient read the message 48 | sleep(5) 49 | resp = imessage.status(guid) 50 | 51 | print(f'Message was read at {resp.get("date_read")}') 52 | 53 | ------------- 54 | Documentation 55 | ------------- 56 | 57 | Sending a message 58 | ----------------- 59 | Send a message to a new or an existing contact! 60 | 61 | **.send(phone, message)** 62 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | 64 | *Args* 65 | 66 | **Phone** | ten-digit phone number of string type format XXXXXXXXXXX i.e. "1234567890" 67 | 68 | *Response* 69 | 70 | **Message** | The message you plan to send. i.e. "Hi!" 71 | 72 | .. list-table:: Returns a **string**, the GUID 73 | :header-rows: 1 74 | 75 | * - Type 76 | - Description 77 | * - string 78 | - GUID unique to the message (used for checking on status) 79 | 80 | Message status 81 | -------------- 82 | 83 | Check whether a message you sent has been delivered and read (if read receipts turned on). 84 | 85 | **.status(guid)** 86 | ~~~~~~~~~~~~~~~~~ 87 | 88 | *Args* 89 | 90 | **Guid** | guid returned from sending a message 91 | 92 | *Response* 93 | 94 | .. list-table:: Returns a **dict**, with following fields 95 | :header-rows: 1 96 | 97 | * - Field 98 | - Type 99 | - Description 100 | - Sample 101 | * - **guid** 102 | - string 103 | - guid that was passed in to the function 104 | - "3A146100-D269-4F35-BDB4-EB2FF7DBDF0F" 105 | * - **date_submitted** 106 | - datetime 107 | - date message was submitted 108 | - "Sun, 12 Apr 2020 05:46:48 GMT" 109 | * - **date_delivered** 110 | - datetime 111 | - date message was delivered to recipient's phone 112 | - "Sun, 12 Apr 2020 05:46:49 GMT" 113 | * - **date_read** 114 | - datetime 115 | - date message was read on recipient's phone 116 | - "Sun, 12 Apr 2020 05:47:38 GMT" 117 | 118 | 119 | Checking iMessage compatibility 120 | ------------------------------- 121 | 122 | Check whether a phone number is registered to an iPhone or an Android device. NOTE: This method is exceptionally slow, so you should cache the response. 123 | 124 | **.check_compatibility(phone)** 125 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 126 | 127 | *Args* 128 | 129 | **Phone** | ten-digit phone number of string type format XXXXXXXXXXX i.e. "1234567890" 130 | 131 | *Response* 132 | 133 | .. list-table:: Returns a **boolean**, compatibility 134 | :header-rows: 1 135 | 136 | * - Type 137 | - Description 138 | * - boolean 139 | - Whether number supports receiving iMessages 140 | 141 | 142 | Contributing 143 | ------------ 144 | Please create an issue. Or feel free to add a PR! 145 | 146 | .. |License| image:: http://img.shields.io/:license-mit-blue.svg 147 | :target: https://pypi.python.org/pypi/Flask-Cors/ 148 | 149 | .. |Downloads| image:: https://pepy.tech/badge/py-imessage 150 | :target: https://pepy.tech/project/py-imessage 151 | 152 | .. |Buy| image:: https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png 153 | :target: https://www.buymeacoffee.com/rolstenhouse 154 | :width: 100px 155 | :height: 50px 156 | -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | To debug/dev for this device 2 | 3 | 1. Use the `test.py` in the root directory to run/execute code 4 | 2. Use the applescript editor and copy/paste the code that's executed in the `osascript` folder 5 | 3. Open the dictionary definition for applescript 6 | 4. console.log outputs to the results window with /* */ 7 | 5. View bottom of check_imessage for code to run 8 | 6. Download uibrowser https://pfiddlesoft.com/uibrowser/ -------------------------------------------------------------------------------- /examples/flask-app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from py_imessage import imessage 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/api/send', methods=['GET', 'POST']) 7 | def send_message(): 8 | # import pdb; pdb.set_trace(); 9 | phone = request.form.get("phone") 10 | text = request.form.get("text") 11 | 12 | if text: 13 | message = text 14 | else: 15 | message = "Thanks for checking out balto!" 16 | # https://stackoverflow.com/questions/5137497/find-current-directory-and-files-directory 17 | # TODO: write this as a better experienc 18 | 19 | guid = imessage.send(phone, text) 20 | 21 | return jsonify({'guid': guid}) 22 | 23 | @app.route("/api/status/", methods=['GET']) 24 | def message_status(guid): 25 | # search local db for the message here and return date_read, and date_delivered 26 | message = imessage.status(guid) 27 | 28 | return jsonify(message) 29 | 30 | @app.route("/api/is_imessage/", methods=['GET']) 31 | def is_imessage_capable(phone): 32 | return jsonify({'is_imessage': imessage.check_compatibility(phone)}) 33 | 34 | @app.route('/') 35 | def index(): 36 | return 'Hello World!' 37 | 38 | if __name__ == '__main__': 39 | app.run(host='127.0.0.1', port=5555) -------------------------------------------------------------------------------- /py_imessage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rolstenhouse/py-iMessage/badc16c5751e602f45b9422f7f8084412a7b1e90/py_imessage/__init__.py -------------------------------------------------------------------------------- /py_imessage/db_conn.py: -------------------------------------------------------------------------------- 1 | # db.py 2 | 3 | # Connect to 4 | import sqlite3 5 | import os 6 | import datetime 7 | import math 8 | import pytz 9 | 10 | # Works for mac Catalina 11 | home = os.environ['HOME'] 12 | db_path = f'{home}/Library/Messages/chat.db' 13 | 14 | db = None 15 | 16 | def open(): 17 | global db 18 | if db: 19 | return db 20 | # Read only mode 21 | db = sqlite3.connect(db_path, uri=True) 22 | 23 | def clean_up(): 24 | if db: 25 | db.close() 26 | 27 | DATE_OFFSET = 978307200 28 | 29 | def apple_time_now(): 30 | return math.floor(datetime.datetime.now() / 1000) - DATE_OFFSET 31 | 32 | def from_apple_time(ts): 33 | if ts==0: 34 | return None 35 | 36 | if unpack_time(ts) != 0: 37 | ts = unpack_time(ts) 38 | 39 | return datetime.datetime.fromtimestamp((ts + DATE_OFFSET), tz=pytz.timezone('US/Pacific')) 40 | 41 | def unpack_time(ts): 42 | return math.floor(ts / (10**9)) 43 | 44 | def pack_time_conditionally(ts): 45 | return ts * 10**9 46 | 47 | def get_most_recently_sent_text(): 48 | return db.execute("""SELECT guid, id as handle, text, date, date_read, date_delivered 49 | FROM message 50 | LEFT OUTER JOIN handle ON message.handle_id=handle.ROWID 51 | WHERE is_from_me = 1 52 | ORDER BY date DESC 53 | LIMIT 1""").fetchone()[0] 54 | 55 | def get_message(guid): 56 | message = db.execute(f"""SELECT guid, date, date_read, date_delivered 57 | FROM message 58 | LEFT OUTER JOIN handle ON message.handle_id=handle.ROWID 59 | WHERE is_from_me = 1 and guid="{guid}" 60 | LIMIT 1""").fetchone() 61 | 62 | return { 63 | 'guid': message[0], 64 | 'date': from_apple_time(message[1]), 65 | 'date_read': from_apple_time(message[2]), 66 | 'date_delivered': from_apple_time(message[3]) 67 | } -------------------------------------------------------------------------------- /py_imessage/imessage.py: -------------------------------------------------------------------------------- 1 | from py_imessage import db_conn 2 | import os 3 | import subprocess 4 | import platform 5 | from time import sleep 6 | from shlex import quote 7 | 8 | import sys 9 | if sys.version_info[0] < 3: 10 | print("Must be using Python 3") 11 | 12 | print(platform.mac_ver()) 13 | 14 | def _check_mac_ver(): 15 | mac_ver, _, _ = platform.mac_ver() 16 | mac_ver = float('.'.join(mac_ver.split('.')[:2])) 17 | if mac_ver >= 10.16: 18 | print(mac_ver) 19 | else: 20 | print("outdated OS") 21 | return mac_ver 22 | 23 | 24 | def send(phone, message): 25 | dir_path = os.path.dirname(os.path.realpath(__file__)) 26 | relative_path = 'osascript/send_message.js' 27 | path = f'{dir_path}/{relative_path}' 28 | cmd = f'osascript -l JavaScript {path} {quote(phone)} {quote(message)}' 29 | subprocess.call(cmd, shell=True) 30 | 31 | # Get chat message db that was sent (look at most recently sent to a phone number) 32 | db_conn.open() 33 | 34 | # Option 1: Loop until result is valid (hard to determine validity without adding other info to the DB) 35 | # Option 2: Sleep for 1 sec to allow local db to update :(( 36 | sleep(1) 37 | guid = db_conn.get_most_recently_sent_text() 38 | return guid 39 | 40 | 41 | def status(guid): 42 | db_conn.open() 43 | message = db_conn.get_message(guid) 44 | return message 45 | 46 | 47 | def check_compatibility(phone): 48 | mac_ver = _check_mac_ver() 49 | is_imessage = False 50 | 51 | dir_path = os.path.dirname(os.path.realpath(__file__)) 52 | relative_path = 'osascript/check_imessage.js' 53 | path = f'{dir_path}/{relative_path}' 54 | cmd = f'osascript -l JavaScript {path} {phone} {mac_ver}' 55 | # Gets all the output from the imessage 56 | output = subprocess.check_output(cmd, shell=True) 57 | 58 | if 'true' in output.decode('utf-8'): 59 | is_imessage = True 60 | 61 | return is_imessage 62 | -------------------------------------------------------------------------------- /py_imessage/osascript/check_imessage.js: -------------------------------------------------------------------------------- 1 | const seApp = Application('System Events'); 2 | seApp.includeStandardAdditions = true; 3 | const messagesApp = Application('Messages'); 4 | messagesApp.includeStandardAdditions = true; 5 | 6 | // Run and get passed in arguments 7 | ObjC.import('stdlib'); // for exit 8 | 9 | var args = $.NSProcessInfo.processInfo.arguments; // NSArray 10 | var argv = []; 11 | var argc = args.count; 12 | for (var i = 4; i < argc; i++) { 13 | argv.push(ObjC.unwrap(args.objectAtIndex(i))); 14 | } 15 | 16 | const number = argv[0]; 17 | const macVer = argv[1]; 18 | checkNumber(number); 19 | 20 | function checkNumber(number) { 21 | messagesApp.activate(); 22 | 23 | delay(0.2); 24 | 25 | seApp.keystroke('n', { using: 'command down' }); 26 | delay(0.1); 27 | seApp.keystroke(number); 28 | delay(0.1); 29 | seApp.keyCode(36); //enter 30 | 31 | delay(2); // Takes a while to realize if it's relevant or not 32 | 33 | // TODO: fix latency here For some reason this is slow as shit 34 | 35 | if (macVer >= 10.16) { 36 | } else { 37 | let base = 38 | seApp.processes['Messages'].windows[0].splitterGroups[0].scrollAreas[2] 39 | .textFields[0]; 40 | base.menuButtons[0].actions['AXShowMenu'].perform(); 41 | let menuItem = base.menus[0].menuItems[0]; 42 | 43 | try { 44 | if (menuItem && menuItem.value() == 'iMessage') { 45 | // Check second message item 46 | let secondMenuItem = base.menus[0].menuItems[1]; 47 | if ( 48 | !secondMenuItem.title() || 49 | !secondMenuItem.title().includes('not registered') 50 | ) { 51 | seApp.keyCode(53); //escape 52 | return true; 53 | } 54 | } 55 | } catch (err) {} 56 | seApp.keyCode(53); //escape 57 | } 58 | return false; 59 | } 60 | 61 | // Use this function for finding which UI element to act on 62 | function debug_applescript() { 63 | /** 64 | * SAMPLE TYPES: buttons | groups | lists | toolbars | uiElements | actions | splitterGroups | textFields | menuButtons 65 | * 66 | * INSTRUCTIONS 67 | * 1. Use the uiElements type as it's the most broad 68 | * 2. Index into the location via the method as shown below 69 | */ 70 | 71 | var buttons = 72 | seApp.processes['Messages'].windows[0].splitterGroups[0].scrollAreas[2] 73 | .textFields; 74 | 75 | for (let i = 0; i < buttons.length; i++) { 76 | console.log(JSON.stringify(buttons[i].properties(), null, 4)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /py_imessage/osascript/send_message.js: -------------------------------------------------------------------------------- 1 | const seApp = Application('System Events') 2 | const messagesApp = Application('Messages') 3 | messagesApp.includeStandardAdditions = true; 4 | 5 | // Run and get passed in arguments 6 | ObjC.import('stdlib') // for exit 7 | 8 | var args = $.NSProcessInfo.processInfo.arguments 9 | var argv = [] 10 | var argc = args.count 11 | for (var i = 4; i < argc; i++) { 12 | // skip 3-word run command at top and this file's name 13 | argv.push(ObjC.unwrap(args.objectAtIndex(i))) 14 | } 15 | 16 | const number = argv[0] 17 | const message = argv[1] 18 | 19 | sendNewMessage(number, message) 20 | 21 | function sendNewMessage(number, message) { 22 | messagesApp.activate() 23 | //EDIT THIS TOO WORK ON SONOMA( tested ) , Current bug: puts number and message into number input 24 | // Adjust delay as necessary 25 | delay(0.2) 26 | 27 | seApp.keystroke('n', {using: 'command down'}) 28 | seApp.keystroke(number) 29 | delay(1); 30 | seApp.keyCode(48) 31 | delay(0.2); 32 | seApp.keyCode(48) 33 | //USE TAB TWICE TOO GO DOWN TOO MESSAGE INPUT 34 | delay(0.2); 35 | seApp.keystroke(message) 36 | delay(0.2); 37 | seApp.keyCode(36) 38 | 39 | return getHandleForNumber(number) 40 | } 41 | 42 | function getHandleForNumber(number) { 43 | // Return handle id associated with number 44 | return messagesApp.buddies.whose({handle:{_contains: number}})[0].id() 45 | } 46 | 47 | 48 | // Should prevent app from quitting 49 | function quit() { 50 | return true; 51 | } 52 | 53 | seApp.keyUp(59) 54 | $.exit(0) 55 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | MarkupSafe==1.1.1 2 | pytz==2019.3 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | setup 4 | ~~~~ 5 | py-iMessage is an extension to let you send/receive iMessages 6 | :copyright: (c) 2020 by Rob Olsthoorn. 7 | :license: MIT, see LICENSE for more details. 8 | """ 9 | 10 | from setuptools import setup 11 | from os.path import join, dirname 12 | 13 | with open (join(dirname(__file__), 'requirements.txt'), 'r') as f: 14 | install_requires = f.read().split("\n") 15 | 16 | setup( 17 | name='py-iMessage', 18 | version=1.7, 19 | url='https://github.com/rolstenhouse/py-imessage', 20 | license='MIT', 21 | author='Rob Olsthoorn', 22 | author_email='rolsthoorn12@gmail.com', 23 | description="Support for sending/receiving iMessages", 24 | long_description=open('README.rst').read(), 25 | packages=['py_imessage'], 26 | zip_safe=False, 27 | python_requires='>=3', 28 | include_package_data=True, 29 | platforms='Operating System :: MacOS :: MacOS X', 30 | install_requires=install_requires, 31 | tests_require=[ 32 | 'nose' 33 | ], 34 | test_suite='nose.collector', 35 | classifiers=[ 36 | 'Environment :: Web Environment', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: MIT License', 39 | 'Operating System :: MacOS :: MacOS X', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.4', 43 | 'Programming Language :: Python :: 3.5', 44 | 'Programming Language :: Python :: 3.6', 45 | 'Programming Language :: Python :: 3.7', 46 | 'Programming Language :: Python :: Implementation :: CPython', 47 | 'Programming Language :: Python :: Implementation :: PyPy', 48 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 49 | 'Topic :: Software Development :: Libraries :: Python Modules' 50 | ] 51 | ) --------------------------------------------------------------------------------