Search code examples
pythongoogle-apigmail-apigoogle-api-python-clientservice-accounts

Send mail in Gmail with python


I'm trying to send a simple test email using the Gmail API but I keep getting the same error for every code sample I find.

My code at the moment is this:

def validationService():
    SCOPES = ['https://mail.google.com/']
    SERVICE_ACCOUNT_FILE = 'creds.json'
    creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)

    service = build('gmail', 'v1', credentials=creds)
    return service


def SendMessage(service, user_id, message):
  try:
    message = (service.users().messages().send(userId=user_id, body=message).execute())
    print('Message Id:',message['id'])
    return message
  except errors.HttpError as error:
    print('An error occurred:', error)


def CreateMessage(sender, to, subject, message_text):
  message = MIMEText(message_text)
  message['to'] = to
  message['from'] = sender
  message['subject'] = subject
  return {'raw': base64.urlsafe_b64encode(message.as_string().encode()).decode()}

service = validationService()

email = CreateMessage("[email protected]", "[email protected]", "Test", "This is a test")

sent = SendMessage(service, "[email protected]", email)

Returns

>>> An error occurred: <HttpError 400 when requesting https://www.googleapis.com/gmail/v1/users/me/messages/send?alt=json returned "Bad Request">

I also don't understand the difference between the "sender" parameter at CreateMessage and userId at SendMessage.

If serves a purpose, I'm using a Service Account credential

- Thanks!


Solution

  • When using a service account with the Gmail API. You need to set domain-wide delegation, which allows the service account to impersonate any user in your G Suite domain. Therefore, you must have a G Suite Account to be able to use domain wide-delegation as the docs say:

    If you have a G Suite domain—if you use G Suite, for example—an administrator of the G Suite domain can authorize an application to access user data on behalf of users in the G Suite domain.

    So now, why do you need to impersonate a user(a real person)? it's due to the fact a service account is a bot(not a real person) that is used to server-to-server interactions making possible your app calls Google APIs and although the service account has a parameter called client_email, which has a structure like [email protected] it's not a real email that belongs to a real person (kind of confusing I know).

    Having said that, I made some changes to your code. First, I modified your validationService function in order to build the service using domain-wide delegation.

    def validationService():
        # Set the crendentials 
        credentials = service_account.Credentials.\
            from_service_account_file(SERVICE_ACCOUNT_FILE, scopes= SCOPES)
        # Delegate the credentials to the user you want to impersonate
        delegated_credentials = credentials.with_subject(USER_EMAIL)
        service = discovery.build('gmail', 'v1', credentials=delegated_credentials)
        return service
    

    In your SendMessage function is not necessarily to pass the user who is going to send the email. Using me is enough as the Users.messages: send Parameters state:

    userId string The user's email address. The special value me can be used to indicate the authenticated user.

    def SendMessage(service, message):
        message = service.users().messages().send(userId="me", body=message).execute()
        return message
    

    Your whole code at the end could look like this one:

    from googleapiclient import discovery, errors
    from oauth2client import file, client, tools
    from google.oauth2 import service_account
    from email.mime.text import MIMEText
    import base64
    
    SERVICE_ACCOUNT_FILE = 'service_account.json'
    SCOPES = [' https://mail.google.com/']
    # The user we want to "impersonate"
    USER_EMAIL = "user@domain"
    
    
    def validationService():
        # Set the crendentials 
        credentials = service_account.Credentials.\
            from_service_account_file(SERVICE_ACCOUNT_FILE, scopes= SCOPES)
        # Delegate the credentials to the user you want to impersonate
        delegated_credentials = credentials.with_subject(USER_EMAIL)
        service = discovery.build('gmail', 'v1', credentials=delegated_credentials)
        return service
    
    
    def SendMessage(service, message):
        message = service.users().messages().send(userId="me", body=message).execute()
        return message
    
    
    def CreateMessage(sender, to, subject, message_text):
      message = MIMEText(message_text)
      message['to'] = to
      message['from'] = sender
      message['subject'] = subject
      return {'raw': base64.urlsafe_b64encode(message.as_string().encode()).decode()}
    
    
    def main():
        try:
            service = validationService()
            email = CreateMessage(USER_EMAIL, "receiverrmail@domain", "Test", "This is a test")
            email_sent = SendMessage(service, email)
            print('Message Id:', email_sent['id'])
        except errors.HttpError as err:
            print('\n---------------You have the following error-------------')
            print(err)
            print('---------------You have the following error-------------\n')
    
    
    if __name__ == '__main__':
        main()
    

    Notice

    You also need to allow your service account to access Google's API when using domain-wide delegation by setting the Managing API client access on your G Suite account.