├── .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 |
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 | )
--------------------------------------------------------------------------------