├── contacts.json ├── requirements.txt ├── .env.template ├── contacts.example.vcf ├── icloud_parser.py ├── Readme.md └── server.py /contacts.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.1 2 | imessage-reader==0.5.0 -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | PASSWORD=password 2 | YOUR_NAME=your-name 3 | PORT_NUMBER=5000 4 | DB_FILEPATH=/Users/$user/Library/Messages/chat.db -------------------------------------------------------------------------------- /contacts.example.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | PRODID:-//Apple Inc.//iOS 16.0//EN 4 | N:;Mark Zuck;;; 5 | FN:Mark Zuck 6 | TEL;type=CELL;type=VOICE;type=pref:+14405656679 7 | REV:2023-04-29T15:44:47Z 8 | END:VCARD 9 | -------------------------------------------------------------------------------- /icloud_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | vcards = open("contacts.vcf","r").read() 5 | def parse_vcards(vcards): 6 | contacts = {} 7 | vcard_list = re.split(r'BEGIN:VCARD\r?\n|END:VCARD', vcards) 8 | 9 | for vcard in vcard_list: 10 | if not vcard.strip(): 11 | continue 12 | 13 | full_name_match = re.search(r'FN:(.+)', vcard) 14 | phone_number_match = re.search(r'TEL;[^:]+:(\+[\d\s\(\)-]+)', vcard) 15 | 16 | if full_name_match and phone_number_match: 17 | full_name = full_name_match.group(1).strip() 18 | phone_number = phone_number_match.group(1).strip() 19 | phone_number = re.sub(r'\D', '', phone_number) # Remove all non-digit characters from the phone number 20 | phone_number= str("+") +phone_number 21 | contacts[phone_number] = full_name 22 | 23 | return contacts 24 | 25 | parsed_contacts = parse_vcards(vcards) 26 | print(json.dumps(parsed_contacts, indent=2)) 27 | with open("contacts.json", "w") as f: 28 | json.dump(parsed_contacts, f, indent=4) -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # iMessage API 2 | 3 | This is a simple API to interact with iMessages using a Flask server. It allows you to send messages, retrieve messages, and fetch recent contacts. 4 | 5 | ## Prerequisites 6 | 7 | - Python 3.x 8 | - Flask 9 | - Access to an iMessage account 10 | - iMessage reader 11 | 12 | ## Installation 13 | 14 | 15 | 1. Clone this repository: 16 | 17 | ``` 18 | git clone https://github.com/danikhan632/iMessage-API.git 19 | ``` 20 | 21 | 2. Change to the directory: 22 | ``` 23 | cd iMessage-api 24 | ``` 25 | 26 | 27 | 3. Install the required packages: 28 | ``` 29 | pip install -r requirements.txt 30 | ``` 31 | 4. Download contacts from iCloud, click select all then export vCard. Rename output and move to this directory 32 | 33 | ![alt text](https://i.imgur.com/47trZvZ.png) 34 | 35 | 5. Enable full disk access on either Terminal or iTerm, whichever one you plan to run the server on. 36 | 37 | ![alt text](https://i.imgur.com/tRkX16J.png) 38 | 39 | 6. Now that you have renamed the iCloud contacts file to contacts.vcf run this to turn the contacts to json 40 | 41 | ``` 42 | python3 icloud_parser.py 43 | ``` 44 | 45 | Now rename ".env.template" to ".env"and set a password, your name, and port number for this to run off. Replace `$user` in `DB_FILEPATH` with your user account name. 46 | ``` 47 | PASSWORD=password 48 | YOUR_NAME=your-name 49 | PORT_NUMBER=5000 50 | DB_FILEPATH=/Users/$user/Library/Messages/chat.db 51 | ``` 52 | 53 | Now to run to run the api server, run the following command 54 | ``` 55 | python3 server.py 56 | ``` 57 | 58 | 59 | # iMessage API Docs 60 | 61 | ## Endpoints 62 | 63 | ### Send a message 64 | 65 | **POST** `/send` 66 | 67 | Send a message to a recipient. 68 | 69 | #### Parameters 70 | 71 | - `recipient`: (Required) The phone number or name of the recipient. 72 | - `message`: (Required) The content of the message. 73 | - `name`: (Optional, default is `true`) If `true`, the recipient parameter will be treated as a name. If `false`, the recipient parameter will be treated as a phone number. 74 | 75 | #### Example 76 | 77 | ```bash 78 | curl -X POST "http://localhost:5000/send" \ 79 | -H "api_key: " \ 80 | -H "Content-Type: application/json" \ 81 | -d '{"recipient": "John Doe", "message": "Hello!"}' 82 | 83 | ``` 84 | 85 | 86 | ### Get messages 87 | **GET** /messages 88 | 89 | Retrieve a list of messages. 90 | 91 | #### Parameters 92 | - 'num_messages': (Optional, default is 10) The number of messages to retrieve. 93 | - 'sent': (Optional, default is true) If true, includes messages sent by you. If false, only includes messages received by you. 94 | - 'formatted': (Optional, default is true) If true, returns messages in a more readable format. 95 | Example 96 | ``` 97 | curl "http://localhost:5000/messages?num_messages=5&sent=true&formatted=true" \ 98 | -H "api_key: " 99 | ``` 100 | 101 | 102 | ### Get most recent contacts 103 | **GET** /recent_contacts 104 | 105 | Retrieve a list of your most recent contacts. 106 | 107 | #### Parameters 108 | - num_contacts: (Optional, default is 10) The number of recent contacts to retrieve. 109 | Example 110 | ``` 111 | curl "http://localhost:5000/recent_contacts?num_contacts=5" 112 | ``` -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import time 2 | import csv 3 | import re 4 | import json 5 | import threading 6 | import os 7 | from datetime import datetime 8 | from flask import Flask, request, jsonify 9 | from imessage_reader import fetch_data 10 | import subprocess 11 | from functools import wraps 12 | from dotenv import load_dotenv 13 | 14 | load_dotenv() 15 | 16 | app = Flask(__name__) 17 | PASSWORD = os.environ.get('PASSWORD') 18 | MY_NAME = os.environ.get('YOUR_NAME') 19 | 20 | def require_api_key(f): 21 | @wraps(f) 22 | def decorated_function(*args, **kwargs): 23 | if request.headers.get('Api-Key') != PASSWORD: 24 | return jsonify({'error': 'Invalid API key'}), 401 25 | return f(*args, **kwargs) 26 | return decorated_function 27 | 28 | global messages 29 | 30 | DB_FILEPATH = os.environ.get('DB_FILEPATH') 31 | 32 | def update_fd(): 33 | global messages 34 | while True: 35 | messages = sorted(fetch_data.FetchData(DB_FILEPATH).get_messages(), key=sort_key, reverse=True) 36 | time.sleep(5) 37 | 38 | threading.Thread(target=update_fd).start() 39 | 40 | contacts = json.load(open("contacts.json", "r")) 41 | reversed_contacts = {value: key for key, value in contacts.items()} 42 | 43 | def getName(phone_number): 44 | try: 45 | if "@" in phone_number: 46 | return phone_number 47 | elif phone_number.startswith("+"): 48 | return contacts[phone_number] 49 | elif len(phone_number) == 10: 50 | return contacts["1" + phone_number] 51 | else: 52 | return contacts["+1" + phone_number] 53 | except KeyError: 54 | return phone_number 55 | 56 | def sort_key(item): 57 | return datetime.strptime(item[2], '%Y-%m-%d %H:%M:%S') 58 | 59 | def send(phone_number, message): 60 | message = message.replace('"', '\\"') 61 | applescript = f''' 62 | tell application "Messages" 63 | set targetService to 1st service whose service type = iMessage 64 | set targetBuddy to buddy "{phone_number}" of targetService 65 | send "{message}" to targetBuddy 66 | end tell 67 | ''' 68 | try: 69 | subprocess.run(['osascript', '-e', applescript]) 70 | except Exception as e: 71 | print(f"Error sending message to {phone_number}: {e}") 72 | 73 | @app.route('/send', methods=['POST']) 74 | @require_api_key 75 | def send_message(): 76 | global messages 77 | try: 78 | isName = request.args.get('name', True) in ['True', 'true'] 79 | data = request.get_json() 80 | if 'recipient' not in data or 'message' not in data: 81 | return jsonify({'error': 'Missing recipient or message in the request'}), 400 82 | 83 | recipient = data['recipient'] 84 | message = data['message'] 85 | if isName: 86 | recipient = getName(data['recipient']) 87 | send(recipient, message) 88 | return jsonify({'status': "ok"}), 200 89 | 90 | except Exception as e: 91 | return jsonify({'error': str(e)}), 500 92 | 93 | @app.route('/messages', methods=['GET']) 94 | @require_api_key 95 | def get_messages(): 96 | global messages 97 | try: 98 | num_messages = int(request.args.get('num_messages', 10)) 99 | sent = request.args.get('sent', True) in ['True', 'true'] 100 | formatted = request.args.get('formatted', True) in ['True', 'true'] 101 | if num_messages > len(messages): 102 | num_messages = len(messages) 103 | if formatted == False: 104 | return jsonify({'messages': messages[:num_messages]}), 200 105 | temp_messages = [] 106 | for i in range(0, num_messages): 107 | sent_by_me = bool(int(messages[i][5])) 108 | name = getName(messages[i][0]) 109 | timestamp = datetime.strptime(messages[i][2], "%Y-%m-%d %H:%M:%S") 110 | if sent_by_me == False: 111 | temp_messages.append({"from": name, "body": messages[i][1], "to": MY_NAME, "datetime": timestamp}) 112 | elif sent_by_me == True and sent == True: 113 | temp_messages.append({"from": MY_NAME, "body": messages[i][1], "to": name, "datetime": timestamp}) 114 | 115 | return jsonify({'messages': temp_messages}), 200 116 | 117 | except Exception as e: 118 | return jsonify({'error': str(e)}), 500 119 | 120 | @app.route('/') 121 | def root(): 122 | print(request) 123 | return jsonify({'messages': "root"}), 200 124 | 125 | @app.route('/messages/', methods=['GET']) 126 | @require_api_key 127 | def get_person_messages(person): 128 | try: 129 | isName = request.args.get('name', True) in ['True', 'true'] 130 | 131 | num_messages = int(request.args.get('num_messages', 10)) 132 | sent = request.args.get('sent', True) in ['True', 'true'] 133 | formatted = request.args.get('formatted', True) in ['True', 'true'] 134 | if num_messages > len(messages): 135 | num_messages = len(messages) 136 | temp_messages = [] 137 | if formatted == False: 138 | for i in range(0, num_messages): 139 | if messages[i][0] == person: 140 | temp_messages.append(messages[i]) 141 | return jsonify({'messages': messages[:num_messages]}), 200 142 | 143 | for i in range(0, num_messages): 144 | sent_by_me = bool(int(messages[i][5])) 145 | name = messages[i][0] 146 | if isName: 147 | name = getName(messages[i][0]) 148 | timestamp = datetime.strptime(messages[i][2], "%Y-%m-%d %H:%M:%S") 149 | if name == person: 150 | if sent_by_me == False: 151 | temp_messages.append({"from": name, "body": messages[i][1], "to": MY_NAME, "datetime": timestamp}) 152 | elif sent_by_me == True and sent == True: 153 | temp_messages.append({"from": MY_NAME, "body": messages[i][1], "to": name, "datetime": timestamp}) 154 | 155 | return jsonify({'messages': temp_messages}), 200 156 | except Exception as e: 157 | return jsonify({'error': str(e)}), 500 158 | 159 | @app.route('/recent_contacts', methods=['GET']) 160 | @require_api_key 161 | def get_most_recent_contacts(): 162 | try: 163 | num_contacts = int(request.args.get('num_contacts', 10)) 164 | recent_contacts = set() 165 | for message in messages: 166 | if len(recent_contacts) >= num_contacts: 167 | break 168 | if bool(int(message[5])): # Sent by me 169 | recent_contacts.add(getName(message[0])) 170 | 171 | return jsonify({'recent_contacts': list(recent_contacts)}), 200 172 | except Exception as e: 173 | return jsonify({'error': str(e)}), 500 174 | 175 | if __name__ == '__main__': 176 | app.run(host='0.0.0.0', port=int(os.environ.get('PORT_NUMBER', '5000'))) 177 | --------------------------------------------------------------------------------