send mail using service account and google api python - python-3.x

Edit:
after accepting the answer:
my question was about a free Gmail account (I didn't know there was a difference) and the answer is about a paid account (and it is a currect one),
the answer showed me that there was a difference and it led me the correct answer to my situation - use a passcode
im trying to send mail using google API and service account, but I'm getting the following erre:
An error occurred: <HttpError 400 when requesting https://gmail.googleapis.com/gmail/v1/users/me/drafts?alt=json returned "Precondition check failed.". Details: "[{'message': 'Precondition check failed.', 'domain': 'global', 'reason': 'failedPrecondition'}]">
this is my code:
from __future__ import print_function
import os.path
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from oauth2client.service_account import ServiceAccountCredentials
import base64
from email.message import EmailMessage
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://mail.google.com/']
def main():
"""Shows basic usage of the Gmail API.
Lists the user's Gmail labels.
"""
creds = None
creds = ServiceAccountCredentials.from_json_keyfile_name(
"""path_to_cred_file.json""", SCOPES)
try:
# Call the Gmail API
service = build('gmail', 'v1', credentials=creds)
message = EmailMessage()
message.set_content('This is automated draft mail')
message['To'] = 'somemail#gmail.com'
message['From'] = 'somemail#gmail.com'
message['Subject'] = 'Automated draft'
# encoded message
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
create_message = {
'message': {
'raw': encoded_message
}
}
# pylint: disable=E1101
draft = service.users().drafts().create(userId="me",
body=create_message).execute()
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f'An error occurred: {error}')
if __name__ == '__main__':
main()

"Precondition check failed" usually means that you're trying to do something that cannot be done. In this case you're trying to send an email from a service account, which is not possible. This answer has a link to a relevant thread from the Google documentation. They say the following:
Service accounts dont work with gmail unless you set up domain wide delegation to a Gsuite account. The reason being is that a service account is its own user you need to delegate its permission to access your gmail account. This will only work with a gsuite domain email address.
This means that the service account by itself cannot send messages, but instead needs to be delegated access to a regular user account in order to send emails. To do this you can add the following line after your creds:
delegated_creds=credentials.with_subject("someuser#yourdomain.com")
#where someuser# is the email of the user that you're sending email as
After that you can use delegated_creds instead of creds to call the service.
Also, you seem to have gotten your sample from Google's guide, but note that your sample creates a draft instead of sending an email. The API call to send emails is a little different. With that in mind here's a complete example based on your code which worked for me:
#all the imports
SCOPES = ['https://mail.google.com/']
def main():
creds = None
creds = ServiceAccountCredentials.from_json_keyfile_name(
"""path_to_cred_file.json""", SCOPES)
delegated_creds=credentials.with_subject("someuser#yourdomain.com")
try:
# Call the Gmail API
service = build('gmail', 'v1', credentials=delegated_creds)
message = EmailMessage()
message.set_content('This is automated draft mail')
message['To'] = 'somemail#gmail.com'
message['From'] = 'somemail#gmail.com'
message['Subject'] = 'Automated draft'
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
create_message = {
'raw': encoded_message
}
email = service.users().messages().send(userId="me",
body=create_message).execute()
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f'An error occurred: {error}')
if __name__ == '__main__':
main()
Finally, as explained in the thread I linked, this only works for Google Workspace accounts and you cannot delegate access to free Gmail accounts, so keep that in mind.

Related

What are the firewall requirements (Port and Sites) for the python Gmail API and OAuth2.0?

I am planning to use the Gmail API for sending emails.
I will also be using OAuth 2.0 authentication.
The mechanism for authenticating is indicated in the quickstart guide:
from __future__ import print_function
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
def main():
"""Shows basic usage of the Gmail API.
Lists the user's Gmail labels.
"""
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
token.write(creds.to_json())
try:
# Call the Gmail API
service = build('gmail', 'v1', credentials=creds)
results = service.users().labels().list(userId='me').execute()
labels = results.get('labels', [])
if not labels:
print('No labels found.')
return
print('Labels:')
for label in labels:
print(label['name'])
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f'An error occurred: {error}')
if __name__ == '__main__':
main()
One example of sending an email:
def gmailAPISendEmail(self, message, userID="me"):
try:
service = self.gmailAPIService
self.GLogger.info("Attempting to send email message")
try:
response = service.users().messages().send(userId=userID, body=message).execute()
except socket.timeout:
pass
except:
self.GLogger.error("Failed to send email message")
tb = traceback.format_exc()
self.GLogger.exception(tb)
try:
responseID = str(response['id'])
except:
responseID = "CouldNotParseID"
self.GLogger.info("Successfully sent email message with ID (" + responseID +")")
return responseID
except:
self.GLogger.error("Failed to send email message")
tb = traceback.format_exc()
self.GLogger.exception(tb)
return False
An example of creating an email:
def createEmaiLWithAttachments(self,sendTo, sendFrom, subject,emailMsg , attachments, html=False):
try:
mimeMessage = MIMEMultipart()
mimeMessage['to'] = sendTo
mimeMessage['from'] = sendFrom
mimeMessage['subject'] = subject
if html:
msg= MIMEText(emailMsg, 'html')
else:
msg= MIMEText(emailMsg)
mimeMessage.attach(msg)
for attachment in attachments:
attachment_name = attachment[0]
attachment_instance = attachment[1]
content_type, encoding = mimetypes.guess_type(attachment_name)
if content_type is None or encoding is not None:
content_type = 'application/octet-stream'
main_type, sub_type = content_type.split('/', 1)
if main_type == 'text':
msg = MIMEText(attachment_instance, _subtype=sub_type)
elif main_type == 'image':
msg = MIMEImage(attachment_instance, _subtype=sub_type)
elif main_type == 'audio':
msg = MIMEAudio(attachment_instance, _subtype=sub_type)
else:
msg = MIMEBase(main_type, sub_type)
msg.set_payload(attachment_instance)
msg.add_header('Content-Disposition', 'attachment', filename=attachment_name)
mimeMessage.attach(msg)
raw_string = base64.urlsafe_b64encode(mimeMessage.as_string().encode()).decode()
theMessage = {'raw': raw_string}
return theMessage
except:
self.GLogger.error("An error occurred in createEmaiLWithAttachments")
tb = traceback.format_exc()
self.GLogger.exception(tb)
return False
I would like to know what the firewall requirements are?
Site/domain requirements
Port requirements
Protocol type (UDP or TCP)
I tried searching for the port requirements and the site/domain requirements, but I didn't find any information for the Gmail API.
I would need to know the firewall requirements for things such as:
Authentication
Refresh credentials
Send email
Fetch emails (messages.list, messages.get, threads.list etc.)
I have opened a Developer Support ticket with Google Workspace Support. An employee from Google who works with APIs has stated the following.
For general API request/response traffic for Google APIs:
Host : *.googleapis.com
Port : 443 & 80
Protocol : TCP
For general authentication traffic for Google APIs:
Host : *.google.com
Port : 443 & 80
Protocol : TCP
The host for the Gmail API request/response traffic specifically, according to here, would be:
Host : gmail.googleapis.com
Port : 443 & 80
Protocol : TCP
The following is information provided by a contributor to the google-api-python-client Github repository.
The python Gmail API uses the endpoints defined in the discovery document for the Gmail V1 API here.
The official support page captures the broader Google Workspace firewall settings.
The support page is where further questions or clarifications can be obtained.
List of IP Addresses needed for Google APIs can be found here. The IP Addresses would need to be allowed for inbound and outbound traffic.
This is another resource for API URL requirements.
Specifically for the Gmail service, there's a similar question here.
Where it makes a reference to Google documentation here and provides the following:
**The asterisk (*) is a wild card, and represents any value except a period**.
*.client-channel.google.com
accounts.google.com
apis.google.com
clients*.google.com
contacts.google.com
hangouts.google.com
*.googleusercontent.com
mail.google.com
ssl.gstatic.com
www.google.com
www.gstatic.com
ogs.google.com
play.google.com
As an alternative, if you do have a Google Workspace subscription and this is implemented through it you can open a Developer Support ticket to check if any additional information can be shared specific to your case.

Getting 401 error when trying to delete gmail messages through API

I have gotten inspiration from
https://developers.google.com/gmail/api/quickstart/python and https://developers.google.com/gmail/api/v1/reference/users/labels/list#examples to get to the following code
from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from apiclient import errors
import requests as req
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://mail.google.com/']
def chunks(l, n):
for i in range(0, len(l), n):
yield l[i:i + n]
def ListMessagesWithLabels(service, user_id, label_ids=[]):
"""List all Messages of the user's mailbox with label_ids applied.
Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
label_ids: Only return Messages with these labelIds applied.
Returns:
List of Messages that have all required Labels applied. Note that the
returned list contains Message IDs, you must use get with the
appropriate id to get the details of a Message.
"""
try:
response = service.users().messages().list(userId=user_id,
labelIds=label_ids).execute()
messages = []
if 'messages' in response:
messages.extend(response['messages'])
while 'nextPageToken' in response:
page_token = response['nextPageToken']
response = service.users().messages().list(userId=user_id,
labelIds=label_ids,
pageToken=page_token).execute()
messages.extend(response['messages'])
return messages
except errors.HttpError as error:
print
'An error occurred: %s' % error
def main():
"""Shows basic usage of the Gmail API.
Lists the user's Gmail labels.
"""
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
service = build('gmail', 'v1', credentials=creds)
# Call the Gmail API
results = service.users().labels().list(userId='me').execute()
labels = results.get('labels', [])
if not labels:
print('No labels found.')
else:
print('Labels:')
for label in labels:
print(label['name'])
if (label["id"].upper() != label["id"]):
messages = ListMessagesWithLabels(service, 'me', [label['id']])
if messages is not None:
usr_id = 'me'
ids = [message["id"] for message in messages]
for x in chunks(ids, 1000):
post_req = req.post(f"https://www.googleapis.com/gmail/v1/users/{usr_id}/messages/batchDelete", data={"ids":x})
if post_req.status_code == 200:
print("all good")
if __name__ == '__main__':
main()
The objective is to go through each label and delete all the messages.
All my POST requests got denied because I am not 'authorized' even though when I start the program I go through the Athentication in the browser etc.
How am I supposed to construct my POSTs to be able to achieve what I want to do ?
When trying to delete the messages, you should be using the service you previously built. That's how you authenticated in the first place. When trying to use batchDelete here:
post_req = req.post(f"https://www.googleapis.com/gmail/v1/users/{usr_id}/messages/batchDelete", data={"ids":x})
You're just doing a basic request to the specified endpoint, you're not following the OAuth process. And because you're not accessing a public resource, you are getting an authorization error.
You should be using something along the following lines instead:
messagesToDelete = {
"ids": [
"messageID1",
"messageID2",
# ... rest of messages to delete
]
}
service.users().messages().batchDelete(userId="me", body=messagesToDelete).execute()
Reference:
Users.messages: batchDelete

Gmail API Reading credentials 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte

I think there is an issue with Gmail API and Python3.
The original code in the documentation is in Python2, but for several reasons, including that my application is already working with Python3, I was passing the code to python3.
So... after sorting several issues, including a 400 request error, (which was apparently that the auth I provided to google wasn't corretly done) I'm facing (I hope) the final issue that apparently I'm trying to read a file but
Even just doing token.read() generates the same issue.
The token.pickle file is autogenerated once you've authorized google to access your email account and that the app can send emails automatically.
I know the credentials.json file is correct, as that's the point to tell google who you are and it always is reading my credentials correctly, redirecting me to give authorization to my app.
Here's the app to send emails, I think it's pretty straightforward, I followed the documentation and looked at other issues like this one and finally got this far:
from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.errors import HttpError
# from httplib2 import Http
# from oauth2client import client, tools, file
import base64
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import mimetypes
from apiclient import errors
SCOPES = 'https://mail.google.com/'
def SendMessage(service, user_id, message):
"""Send an email message.
Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
message: Message to be sent.
Returns:
Sent Message.
"""
try:
message = (service.users().messages().send(userId=user_id, body=message)
.execute())
print(f'Message Id: {message["id"]}')
return message
except errors.HttpError as error:
print(f'An error occurred: {error}')
def CreateMessage(sender, to, subject, message_text):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
message_text: The text of the email message.
Returns:
An object containing a base64url encoded email object.
"""
message = MIMEText(message_text)
message['to'] = to
message['from'] = sender
message['subject'] = subject
message_bytes = message.as_string().encode('utf-8')
# return { 'raw': base64.urlsafe_b64encode(message.as_string()) }
# return { 'raw': base64.urlsafe_b64encode(message_bytes) }
raw = base64.urlsafe_b64encode(message_bytes)
return raw.decode()
def main():
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'r') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
service = build('gmail', 'v1', credentials=creds)
# Create
message = CreateMessage('my-email#gmail.com', 'destination#gmail.com', 'Testing Gmail API', 'Hi GMAIL API')
# Send
SendMessage(service, "me", message)
if __name__ == '__main__':
main()
I truly don't know what to do here, if someone has already solved this issue and how.
THANKS!
In your script, when token.pickle is not created, Gmail API can be used by creating token.pickle at 1st authorization. But an error of 'raw' RFC822 payload message string or uploading message via /upload/* URL required occurs at message = (service.users().messages().send(userId=user_id, body=message).execute()). So in this case, please modify as follows.
From:
raw = base64.urlsafe_b64encode(message_bytes)
return raw.decode()
To:
raw = base64.urlsafe_b64encode(message_bytes).decode('utf-8')
return {'raw': raw}
After above modification, when your script is run as the 2nd run, when token.pickle, which has already been created, is read, an error occurs. I think that this error is in your title. In this case, please modify as follows.
From:
with open('token.pickle', 'r') as token:
To:
with open('token.pickle', 'rb') as token:
By this, I think that the script works.
By the way, if an error occurs when the code is retrieved, also please modify as follows.
From:
creds = flow.run_local_server(port=0)
To:
creds = flow.run_local_server()
Note:
About the authorization, you can see the sample script at the Quickstart of the official document.
If this was not the direction of your issue, I apologize.

How to interact with the Gmail API - Delegate User - Python 3.7

I am trying to write a Python application which simply adds a user as a delegate to another users mailbox.
I am following the API # Google API Documentation - Users.settings.delegates: create
However, I am struggling to find how to the parameters of:
User - an account which is TOBE added to a delegate Mailbox
Mailbox - the account which has the Mailbox I wish the account to become a delegate of.
I have currently tried making an API which has the delegate user. However, it does not seem to be interacting how I would expect. I am hoping Google will create a responsive API for the browser to support this. However, I am struggling with the code:
from googleapiclient import discovery
from oauth2client.service_account import ServiceAccountCredentials
def main(user_to_be_added, delegated_mailbox):
service_account_credentials = ServiceAccountCredentials.from_json_keyfile_name('credentials/service_account.json')
service_account_credentials = service_account_credentials.create_scoped('https://mail.google.com/ https://www.googleapis.com/auth/gmail.insert https://www.googleapis.com/auth/gmail.modify')
service_account_credentials = service_account_credentials.create_delegated(user_to_be_added)
service = discovery.build('gmail', 'v1', credentials=service_account_credentials)
response = service.users().settings().delegates().create().execute(userId=delegated_mailbox)
if __name__ == '__main__':
main('some_account_to_be_added#gmail.com', 'delegated_mailbox#gmail.com')
Am I interacting with this API completely wrong? If so, how has anyone else achieved this?
Thank you for your time.
Jordan
Working Solution:
from googleapiclient import discovery
from google.oauth2 import service_account
def _create_client(subject):
credentials = service_account.Credentials
credentials = credentials.from_service_account_file('credentials/service_account.json',
scopes=['https://www.googleapis.com/auth/gmail.settings.sharing',
'https://www.googleapis.com/auth/gmail.settings.basic'],
subject=subject)
service = discovery.build('gmail', 'v1', credentials=credentials)
return service
def add_delegate_to_email(user_to_be_added, delegated_mailbox):
service = _create_client(user_to_be_added)
body = {
"delegateEmail": delegated_mailbox,
"verificationStatus": "accepted"
}
try:
response = service.users().settings().delegates().create(userId='me', body=body).execute()
print(response)
except Exception as e:
print('Exception: {}'.format(e))
Main problem: from oauth2client.service_account import ServiceAccountCredentials is deprecated as Google took ownership with google-auth.

How do you programmatically get a list of Google Forms (with ids) using Google Forms API?

I would like to know if it is possible to get the list of a Google Drive user's Forms (not the Docs, Spreadsheets, ...) using the Forms Service or the Google Drive API.
The reason why I am not sure is because there is no indication on how to do this on the Forms Service page, and on the Drive API's page the indications are very general. They don't take into account the fact that I will be using a OAuth2 token with a 'forms' scope and not a 'drive' scope.
/* List all Google Forms in your Google Drive */
function getGoogleFormsList() {
var files = DriveApp.getFilesByType(MimeType.GOOGLE_FORMS);
while (files.hasNext()) {
var file = files.next();
Logger.log("%s %s %s", file.getName(), file.getId(), file.getUrl());
}
}
Python solution:
from __future__ import print_function
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly']
def main():
"""Shows basic usage of the Drive v3 API.
Prints the names and ids of the first 10 files the user has access to.
"""
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'client_secrets.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
token.write(creds.to_json())
try:
page_token = None
drive_service = build('drive', 'v3', credentials=creds)
while True:
response = drive_service.files().list(
# You can use MIME types to filter query results
q="mimeType='application/vnd.google-apps.form'",
spaces='drive',
fields='nextPageToken, files(id, name)',
pageToken=page_token
).execute()
for file in response.get('files', []):
# Process change
print ('Found file: %s (%s)' % (file.get('name'), file.get('id')))
page_token = response.get('nextPageToken', None)
if page_token is None:
break
except HttpError as error:
# TODO(developer) - Handle errors from drive API.
print(f'An error occurred: {error}')
if __name__ == '__main__':
main()

Resources