├── .gitignore ├── requirements.txt ├── README.md ├── gpt.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | credentials.json 3 | token.pickle 4 | venv/ 5 | context.txt 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | google-api-python-client 2 | google-auth-httplib2 3 | google-auth-oauthlib 4 | openai 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gmail AI Assistant 2 | 3 | This is a Python script that will read all your unread emails and reply to them automatically with a ChatGPT generated response. 4 | 5 | ## Quick start 6 | 7 | ```bash 8 | $ python3 -m venv venv 9 | $ source venv/bin/activate 10 | $ pip install -r requirements.txt 11 | $ export OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXX 12 | $ python3 main.py 13 | ``` 14 | 15 | The script will do an OAuth authentication for your Gmail account and then directly read all your unread emails and **automatically respond** to them with a ChatGPT generated response. 16 | 17 | You can pass a text file with instructions for how to reply to emails as a command line argument: 18 | 19 | ```bash 20 | $ python3 main.py context.txt 21 | ``` 22 | -------------------------------------------------------------------------------- /gpt.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | 3 | client = OpenAI() 4 | 5 | def make_reply(email, context=""): 6 | system_message = "You are an automatic email replyer. Write a response to the given email conversation. Respond only with the reply email, nothing else." 7 | 8 | if context: 9 | system_message += "\n\n" + "This is your current knowledge as an assistant. Adhere to these rules when creating your response:\n\n" + context 10 | 11 | response = client.chat.completions.create( 12 | model="gpt-3.5-turbo-0125", 13 | messages=[ 14 | { 15 | "role": "system", 16 | "content": system_message, 17 | }, 18 | { 19 | "role": "user", 20 | "content": email 21 | } 22 | ] 23 | ) 24 | 25 | response_message = response.choices[0].message 26 | message_content = response_message.content 27 | 28 | return message_content 29 | 30 | 31 | if __name__ == "__main__": 32 | reply = make_reply("Hello!\nI would like to book an appointment for a meeting\n\nBest,\nMe") 33 | print(reply) 34 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from google_auth_oauthlib.flow import InstalledAppFlow 2 | from google.auth.transport.requests import Request 3 | from googleapiclient.discovery import build 4 | from email.mime.text import MIMEText 5 | import pickle 6 | import base64 7 | import email 8 | import time 9 | import sys 10 | import os 11 | 12 | import gpt 13 | 14 | # If modifying these scopes, delete the file token.pickle. 15 | SCOPES = [ 16 | 'https://www.googleapis.com/auth/gmail.readonly', 17 | 'https://www.googleapis.com/auth/gmail.send', 18 | 'https://www.googleapis.com/auth/gmail.modify', 19 | 'https://www.googleapis.com/auth/userinfo.email', 20 | 'https://www.googleapis.com/auth/userinfo.profile', 21 | 'openid', 22 | ] 23 | 24 | 25 | def get_services(): 26 | creds = None 27 | # The file token.pickle stores the user's access and refresh tokens, and is 28 | # created automatically when the authorization flow completes for the first 29 | # time. 30 | if os.path.exists('token.pickle'): 31 | with open('token.pickle', 'rb') as token: 32 | creds = pickle.load(token) 33 | 34 | # If there are no (valid) credentials available, let the user log in. 35 | if not creds or not creds.valid: 36 | if creds and creds.expired and creds.refresh_token: 37 | creds.refresh(Request()) 38 | else: 39 | flow = InstalledAppFlow.from_client_secrets_file( 40 | 'credentials.json', SCOPES) 41 | creds = flow.run_local_server(port=0) 42 | # Save the credentials for the next run 43 | with open('token.pickle', 'wb') as token: 44 | pickle.dump(creds, token) 45 | 46 | return { 47 | "gmail": build('gmail', 'v1', credentials=creds), 48 | "people": build('people', 'v1', credentials=creds), 49 | } 50 | 51 | 52 | def get_user_info(service): 53 | """ 54 | Fetches the user's name and email address. 55 | 56 | Args: 57 | service: Authorized People API service instance. 58 | 59 | Returns: 60 | A dictionary containing the user's name and email address. 61 | """ 62 | profile = service.people().get(resourceName='people/me', personFields='names,emailAddresses').execute() 63 | name = profile['names'][0]['displayName'] 64 | email = profile['emailAddresses'][0]['value'] 65 | return {"name": name, "email": email} 66 | 67 | 68 | def create_message(sender, to, subject, message_text, headers=None): 69 | message = MIMEText(message_text) 70 | message['to'] = to 71 | message['from'] = sender 72 | message['subject'] = subject 73 | if headers: 74 | for name, value in headers.items(): 75 | message[name] = value 76 | raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode() 77 | return {'raw': raw_message} 78 | 79 | 80 | def send_message(service, message): 81 | message = service.users().messages().send(userId='me', body=message).execute() 82 | print(f"Sent message: {message['id']}") 83 | return message 84 | 85 | 86 | def get_message_body(message): 87 | msg_raw = base64.urlsafe_b64decode(message['raw'].encode('ASCII')) 88 | msg_str = email.message_from_bytes(msg_raw) 89 | 90 | # If message is multipart, the parts are in .get_payload() 91 | if msg_str.is_multipart(): 92 | for part in msg_str.walk(): 93 | if part.get_content_type() == 'text/plain': 94 | # We use get_payload(decode=True) to decode the base64 string 95 | body = part.get_payload(decode=True).decode() 96 | return body 97 | else: 98 | # If message is not multipart, simply decode 99 | body = msg_str.get_payload(decode=True).decode() 100 | return body 101 | 102 | 103 | def mark_message_as_read(service, msg_id): 104 | """ 105 | Marks the specified message as read by removing the 'UNREAD' label. 106 | 107 | Args: 108 | service: Authorized Gmail API service instance. 109 | msg_id: The ID of the message to mark as read. 110 | """ 111 | service.users().messages().modify( 112 | userId="me", 113 | id=msg_id, 114 | body={'removeLabelIds': ['UNREAD']} 115 | ).execute() 116 | print(f"Message ID {msg_id} marked as read.") 117 | 118 | 119 | def get_messages(service): 120 | # Call the Gmail API 121 | results = service.users().messages().list(userId='me', labelIds=['UNREAD']).execute() 122 | messages = results.get('messages', []) 123 | 124 | message_list = [] 125 | 126 | for message in messages: 127 | msg_raw = service.users().messages().get(userId='me', id=message['id'], format="raw").execute() 128 | msg_json = service.users().messages().get(userId='me', id=message['id']).execute() 129 | 130 | headers = msg_json["payload"]["headers"] 131 | for header in headers: 132 | if header["name"] == "From": 133 | sender = header["value"] 134 | sender_name = sender.split("<")[0].strip() 135 | sender_email = sender.split("<")[1].strip().removesuffix(">") 136 | msg_json["sender_name"] = sender_name 137 | msg_json["sender_email"] = sender_email 138 | if header["name"] == "Subject": 139 | msg_json["subject"] = header["value"] 140 | if header["name"] == "Message-ID": 141 | msg_json["message_id"] = header["value"] 142 | 143 | message_list.append({ 144 | "raw": msg_raw, 145 | "json": msg_json, 146 | }) 147 | 148 | return message_list 149 | 150 | 151 | def reply_to_unread_messages(gmail_service, context, info): 152 | print("Getting unread messages...") 153 | messages = get_messages(gmail_service) 154 | 155 | for message in messages: 156 | raw_email = get_message_body(message["raw"]) 157 | 158 | print("Generating GPT reply...") 159 | reply = gpt.make_reply(raw_email, context) 160 | 161 | current_date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) 162 | sender_name = message["json"]["sender_name"] 163 | sender_email = message["json"]["sender_email"] 164 | subject = message["json"]["subject"] 165 | message_id = message["json"]["message_id"] 166 | 167 | reply_block = "\n".join(["> " + line for line in raw_email.split("\n")]) 168 | 169 | raw_reply = reply + "\n\nOn "+current_date+" "+sender_name+" <"+sender_email+"> wrote:\n\n"+reply_block 170 | 171 | my_info = info["name"] + " <"+info["email"]+">" 172 | 173 | re_subject = "Re: " + subject.removeprefix("Re: ") 174 | 175 | headers = { 176 | "In-Reply-To": message_id, 177 | "References": message_id, 178 | } 179 | 180 | new_message = create_message(my_info, sender_email, re_subject, raw_reply, headers) 181 | 182 | print("Sending reply...") 183 | send_message(gmail_service, new_message) 184 | 185 | print("Marking message as read...") 186 | mark_message_as_read(gmail_service, message["json"]["id"]) 187 | 188 | print("Done!") 189 | 190 | if len(messages) == 0: 191 | print("No unread messages found.") 192 | else: 193 | print(f"Replied to {len(messages)} messages.") 194 | 195 | 196 | def main(): 197 | if len(sys.argv) == 2: 198 | with open(sys.argv[1], "r") as f: 199 | context = f.read() 200 | else: 201 | context = "" 202 | 203 | print( "#######################################" ) 204 | print( "## Gmail AI Assistant by ##" ) 205 | print( "## Unconventional Coding ##" ) 206 | print( "## ##" ) 207 | print( "## WARNING: ##") 208 | print( "## This program will fetch unread ##" ) 209 | print( "## messages from your Gmail inbox ##" ) 210 | print( "## and reply to them using GPT-3.5 ##" ) 211 | print( "## ##" ) 212 | print( "## Type 'yes' to continue ##" ) 213 | print( "#######################################" ) 214 | 215 | if input() != "yes": 216 | print("Exiting...") 217 | sys.exit() 218 | 219 | print("Authenticating...") 220 | services = get_services() 221 | 222 | gmail_service = services["gmail"] 223 | people_service = services["people"] 224 | 225 | print("Getting user info...") 226 | info = get_user_info(people_service) 227 | 228 | while True: 229 | reply_to_unread_messages(gmail_service, context, info) 230 | print("Waiting for 10 seconds...") 231 | time.sleep(10) 232 | 233 | if __name__ == '__main__': 234 | main() 235 | --------------------------------------------------------------------------------