Skip to content

Commit 89a51e8

Browse files
committed
add gmail api tutorial
1 parent 067e76d commit 89a51e8

10 files changed

+812
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ This is a repository of all the tutorials of [The Python Code](https://www.thepy
130130
- [How to Make a URL Shortener in Python](https://www.thepythoncode.com/article/make-url-shortener-in-python). ([code](general/url-shortener))
131131
- [How to Get Google Page Ranking in Python](https://www.thepythoncode.com/article/get-google-page-ranking-by-keyword-in-python). ([code](general/getting-google-page-ranking))
132132
- [How to Make a Telegram Bot in Python](https://www.thepythoncode.com/article/make-a-telegram-bot-in-python). ([code](general/telegram-bot))
133+
- [How to Use Gmail API in Python](https://www.thepythoncode.com/article/use-gmail-api-in-python). ([code](general/gmail-api))
133134

134135
- ### [Database](https://www.thepythoncode.com/topic/using-databases-in-python)
135136
- [How to Use MySQL Database in Python](https://www.thepythoncode.com/article/using-mysql-database-in-python). ([code](database/mysql-connector))

general/gmail-api/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# [How to Use Gmail API in Python](https://www.thepythoncode.com/article/use-gmail-api-in-python)
2+
To use the scripts here:
3+
- Create your credentials file in Google API dashboard and putting it in the current directory, follow [this tutorial](https://www.thepythoncode.com/article/use-gmail-api-in-python) for detailed information.
4+
- `pip3 install -r requirements.txt`
5+
- Change `our_email` variable in `common.py` to your gmail address.
6+
- To send emails, use the `send_emails.py` script:
7+
```
8+
python send_emails.py --help
9+
```
10+
**Output:**
11+
```
12+
usage: send_emails.py [-h] [-f FILES [FILES ...]] destination subject body
13+
14+
Email Sender using Gmail API
15+
16+
positional arguments:
17+
destination The destination email address
18+
subject The subject of the email
19+
body The body of the email
20+
21+
optional arguments:
22+
-h, --help show this help message and exit
23+
-f FILES [FILES ...], --files FILES [FILES ...]
24+
email attachments
25+
```
26+
For example, sending to example@domain.com:
27+
```
28+
python send_emails.py example@domain.com "This is a subject" "Body of the email" --files file1.pdf file2.txt file3.img
29+
```
30+
- To read emails, use the `read_emails.py` script. Downloading & parsing emails for Python related emails:
31+
```
32+
python read_emails.py "python"
33+
```
34+
This will output basic information on all matched emails and creates a folder for each email along with attachments and HTML version of the emails.
35+
- To mark emails as **read** or **unread**, consider using `mark_emails.py`:
36+
```
37+
python mark_emails.py --help
38+
```
39+
**Output**:
40+
```
41+
usage: mark_emails.py [-h] [-r] [-u] query
42+
43+
Marks a set of emails as read or unread
44+
45+
positional arguments:
46+
query a search query that selects emails to mark
47+
48+
optional arguments:
49+
-h, --help show this help message and exit
50+
-r, --read Whether to mark the message as read
51+
-u, --unread Whether to mark the message as unread
52+
```
53+
Marking emails from **Google Alerts** as **Read**:
54+
```
55+
python mark_emails.py "Google Alerts" --read
56+
```
57+
Marking emails sent from example@domain.com as **Unread**:
58+
```
59+
python mark_emails.py "example@domain.com" -u
60+
```
61+
- To delete emails, consider using `delete_emails.py` script, e.g: for deleting emails about Bitcoin:
62+
```
63+
python delete_emails.py "bitcoin"
64+
```
65+
- If you want the full code, consider using `tutorial.ipynb` file.
66+
- Or if you want a all-in-one script, `gmail_api.py` is here as well!

general/gmail-api/common.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
import pickle
3+
# Gmail API utils
4+
from googleapiclient.discovery import build
5+
from google_auth_oauthlib.flow import InstalledAppFlow
6+
from google.auth.transport.requests import Request
7+
8+
# Request all access (permission to read/send/receive emails, manage the inbox, and more)
9+
SCOPES = ['https://mail.google.com/']
10+
our_email = 'our_email@gmail.com'
11+
12+
13+
def gmail_authenticate():
14+
creds = None
15+
# the file token.pickle stores the user's access and refresh tokens, and is
16+
# created automatically when the authorization flow completes for the first time
17+
if os.path.exists("token.pickle"):
18+
with open("token.pickle", "rb") as token:
19+
creds = pickle.load(token)
20+
# if there are no (valid) credentials availablle, let the user log in.
21+
if not creds or not creds.valid:
22+
if creds and creds.expired and creds.refresh_token:
23+
creds.refresh(Request())
24+
else:
25+
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
26+
creds = flow.run_local_server(port=0)
27+
# save the credentials for the next run
28+
with open("token.pickle", "wb") as token:
29+
pickle.dump(creds, token)
30+
return build('gmail', 'v1', credentials=creds)
31+
32+
33+
def search_messages(service, query):
34+
result = service.users().messages().list(userId='me',q=query).execute()
35+
messages = [ ]
36+
if 'messages' in result:
37+
messages.extend(result['messages'])
38+
while 'nextPageToken' in result:
39+
page_token = result['nextPageToken']
40+
result = service.users().messages().list(userId='me',q=query, pageToken=page_token).execute()
41+
if 'messages' in result:
42+
messages.extend(result['messages'])
43+
return messages

general/gmail-api/delete_emails.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from common import gmail_authenticate, search_messages
2+
3+
def delete_messages(service, query):
4+
messages_to_delete = search_messages(service, query)
5+
# it's possible to delete a single message with the delete API, like this:
6+
# service.users().messages().delete(userId='me', id=msg['id'])
7+
# but it's also possible to delete all the selected messages with one query, batchDelete
8+
return service.users().messages().batchDelete(
9+
userId='me',
10+
body={
11+
'ids': [ msg['id'] for msg in messages_to_delete]
12+
}
13+
).execute()
14+
15+
if __name__ == "__main__":
16+
import sys
17+
service = gmail_authenticate()
18+
delete_messages(service, sys.argv[1])

general/gmail-api/gmail_api.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# for parsing commandline arguments
2+
import argparse
3+
from common import search_messages, gmail_authenticate
4+
from read_emails import read_message
5+
from send_emails import send_message
6+
from delete_emails import delete_messages
7+
from mark_emails import mark_as_read, mark_as_unread
8+
9+
10+
if __name__ == '__main__':
11+
parser = argparse.ArgumentParser(description="Send/Search/Delete/Mark messages using gmail's API.")
12+
subparsers = parser.add_subparsers(help='Subcommands')
13+
parser_1 = subparsers.add_parser('send', help='Send an email')
14+
parser_1.add_argument('destination', type=str, help='The destination email address')
15+
parser_1.add_argument('subject', type=str, help='The subject of the email')
16+
parser_1.add_argument('body', type=str, help='The body of the email')
17+
parser_1.add_argument('files', type=str, help='email attachments', nargs='+')
18+
parser_1.set_defaults(action='send')
19+
parser_2 = subparsers.add_parser('delete', help='Delete a set of emails')
20+
parser_2.add_argument('query', type=str, help='a search query that selects emails to delete')
21+
parser_2.set_defaults(action='delete')
22+
parser_3 = subparsers.add_parser('mark', help='Marks a set of emails as read or unread')
23+
parser_3.add_argument('query', type=str, help='a search query that selects emails to mark')
24+
parser_3.add_argument('read_status', type=bool, help='Whether to mark the message as unread, or as read')
25+
parser_3.set_defaults(action='mark')
26+
parser_4 = subparsers.add_parser('search', help='Marks a set of emails as read or unread')
27+
parser_4.add_argument('query', type=str, help='a search query, which messages to display')
28+
parser_4.set_defaults(action='search')
29+
args = parser.parse_args()
30+
service = gmail_authenticate()
31+
if args.action == 'send':
32+
# TODO: add attachements
33+
send_message(service, args.destination, args.subject, args.body, args.files)
34+
elif args.action == 'delete':
35+
delete_messages(service, args.query)
36+
elif args.action == 'mark':
37+
print(args.unread_status)
38+
if args.read_status:
39+
mark_as_read(service, args.query)
40+
else:
41+
mark_as_unread(service, args.query)
42+
elif args.action == 'search':
43+
results = search_messages(service, args.query)
44+
for msg in results:
45+
read_message(service, msg)

general/gmail-api/mark_emails.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from common import gmail_authenticate, search_messages
2+
3+
def mark_as_read(service, query):
4+
messages_to_mark = search_messages(service, query)
5+
return service.users().messages().batchModify(
6+
userId='me',
7+
body={
8+
'ids': [ msg['id'] for msg in messages_to_mark ],
9+
'removeLabelIds': ['UNREAD']
10+
}
11+
).execute()
12+
13+
def mark_as_unread(service, query):
14+
messages_to_mark = search_messages(service, query)
15+
return service.users().messages().batchModify(
16+
userId='me',
17+
body={
18+
'ids': [ msg['id'] for msg in messages_to_mark ],
19+
'addLabelIds': ['UNREAD']
20+
}
21+
).execute()
22+
23+
if __name__ == "__main__":
24+
import argparse
25+
parser = argparse.ArgumentParser(description="Marks a set of emails as read or unread")
26+
parser.add_argument('query', help='a search query that selects emails to mark')
27+
parser.add_argument("-r", "--read", action="store_true", help='Whether to mark the message as read')
28+
parser.add_argument("-u", "--unread", action="store_true", help='Whether to mark the message as unread')
29+
30+
args = parser.parse_args()
31+
service = gmail_authenticate()
32+
if args.read:
33+
mark_as_read(service, args.query)
34+
elif args.unread:
35+
mark_as_unread(service, args.query)

general/gmail-api/read_emails.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import os
2+
import sys
3+
# for encoding/decoding messages in base64
4+
from base64 import urlsafe_b64decode
5+
from common import gmail_authenticate, search_messages
6+
7+
8+
def get_size_format(b, factor=1024, suffix="B"):
9+
"""
10+
Scale bytes to its proper byte format
11+
e.g:
12+
1253656 => '1.20MB'
13+
1253656678 => '1.17GB'
14+
"""
15+
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
16+
if b < factor:
17+
return f"{b:.2f}{unit}{suffix}"
18+
b /= factor
19+
return f"{b:.2f}Y{suffix}"
20+
21+
22+
def clean(text):
23+
# clean text for creating a folder
24+
return "".join(c if c.isalnum() else "_" for c in text)
25+
26+
27+
def parse_parts(service, parts, folder_name):
28+
"""
29+
Utility function that parses the content of an email partition
30+
"""
31+
if parts:
32+
for part in parts:
33+
filename = part.get("filename")
34+
mimeType = part.get("mimeType")
35+
body = part.get("body")
36+
data = body.get("data")
37+
file_size = body.get("size")
38+
part_headers = part.get("headers")
39+
if part.get("parts"):
40+
# recursively call this function when we see that a part
41+
# has parts inside
42+
parse_parts(service, part.get("parts"), folder_name)
43+
if mimeType == "text/plain":
44+
# if the email part is text plain
45+
if data:
46+
text = urlsafe_b64decode(data).decode()
47+
print(text)
48+
elif mimeType == "text/html":
49+
# if the email part is an HTML content
50+
# save the HTML file and optionally open it in the browser
51+
if not filename:
52+
filename = "index.html"
53+
filepath = os.path.join(folder_name, filename)
54+
print("Saving HTML to", filepath)
55+
with open(filepath, "wb") as f:
56+
f.write(urlsafe_b64decode(data))
57+
else:
58+
# attachment other than a plain text or HTML
59+
for part_header in part_headers:
60+
part_header_name = part_header.get("name")
61+
part_header_value = part_header.get("value")
62+
if part_header_name == "Content-Disposition":
63+
if "attachment" in part_header_value:
64+
# we get the attachment ID
65+
# and make another request to get the attachment itself
66+
print("Saving the file:", filename, "size:", get_size_format(file_size))
67+
attachment_id = body.get("attachmentId")
68+
attachment = service.users().messages() \
69+
.attachments().get(id=attachment_id, userId='me', messageId=msg['id']).execute()
70+
data = attachment.get("data")
71+
filepath = os.path.join(folder_name, filename)
72+
if data:
73+
with open(filepath, "wb") as f:
74+
f.write(urlsafe_b64decode(data))
75+
76+
77+
def read_message(service, message_id):
78+
"""
79+
This function takes Gmail API `service` and the given `message_id` and does the following:
80+
- Downloads the content of the email
81+
- Prints email basic information (To, From, Subject & Date) and plain/text parts
82+
- Creates a folder for each email based on the subject
83+
- Downloads text/html content (if available) and saves it under the folder created as index.html
84+
- Downloads any file that is attached to the email and saves it in the folder created
85+
"""
86+
msg = service.users().messages().get(userId='me', id=message_id['id'], format='full').execute()
87+
# parts can be the message body, or attachments
88+
payload = msg['payload']
89+
headers = payload.get("headers")
90+
parts = payload.get("parts")
91+
folder_name = "email"
92+
if headers:
93+
# this section prints email basic info & creates a folder for the email
94+
for header in headers:
95+
name = header.get("name")
96+
value = header.get("value")
97+
if name == 'From':
98+
# we print the From address
99+
print("From:", value)
100+
if name == "To":
101+
# we print the To address
102+
print("To:", value)
103+
if name == "Subject":
104+
# make a directory with the name of the subject
105+
folder_name = clean(value)
106+
# we will also handle emails with the same subject name
107+
folder_counter = 0
108+
while os.path.isdir(folder_name):
109+
folder_counter += 1
110+
# we have the same folder name, add a number next to it
111+
if folder_name[-1].isdigit() and folder_name[-2] == "_":
112+
folder_name = f"{folder_name[:-2]}_{folder_counter}"
113+
elif folder_name[-2:].isdigit() and folder_name[-3] == "_":
114+
folder_name = f"{folder_name[:-3]}_{folder_counter}"
115+
else:
116+
folder_name = f"{folder_name}_{folder_counter}"
117+
os.mkdir(folder_name)
118+
print("Subject:", value)
119+
if name == "Date":
120+
# we print the date when the message was sent
121+
print("Date:", value)
122+
123+
parse_parts(service, parts, folder_name)
124+
print("="*50)
125+
126+
if __name__ == "__main__":
127+
service = gmail_authenticate()
128+
# get emails that match the query you specify from the command lines
129+
results = search_messages(service, sys.argv[1])
130+
# for each email matched, read it (output plain/text to console & save HTML and attachments)
131+
for msg in results:
132+
read_message(service, msg)

general/gmail-api/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
google-api-python-client
2+
google-auth-httplib2
3+
google-auth-oauthlib

0 commit comments

Comments
 (0)