Search code examples
pythonemailsmtpgmailimap

Connect to Gmail Using Email Address and Password with Python


I am trying to connect to my Gmail account using Python. I want to connect to it using both SMTP and IMAP. I am aware that it is possible to use an app password to make this connection, but is there a way to use the actual email password instead?

I have been reading this particular article,
https://support.google.com/accounts/answer/6010255#zippy=%2Cif-less-secure-app-access-is-on-for-your-account%2Cif-less-secure-app-access-is-off-for-your-account

The warning given at the top tells me that it is not possible to do so, even if 'Less secure app access' is allowed,

enter image description here

Do I have that right?

Given below is the code I am using to send and search emails. Like I said though, this only works with an app password,

import smtplib
import imaplib
import email
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import pandas as pd


class EmailClient:
    def __init__(self, email, password, smtp_server='smtp.gmail.com', smtp_port=587, imap_server="imap.gmail.com"):
    self.email = email
    self.password = password
    self.smtp_server = smtplib.SMTP(smtp_server, smtp_port)
    self.imap_server = imaplib.IMAP4_SSL(imap_server)

def send_email(self, to_addr, subject, body):
    msg = MIMEMultipart()
    msg['From'] = self.email
    msg['To'] = to_addr
    msg['Subject'] = subject
    msg.attach(MIMEText(body, 'plain'))

    self.smtp_server.starttls()
    self.smtp_server.login(self.email, self.password)
    self.smtp_server.send_message(msg)
    self.smtp_server.quit()

def search_email(self, mailbox="INBOX", subject=None, to=None, from_=None, since_date=None, until_date=None,
                 since_emailid=None):

    self.imap_server.login(self.email, self.password)

    self.imap_server.select(mailbox)

    query_parts = []
    if subject is not None:
        query_parts.append(f'(SUBJECT "{subject}")')
    if to is not None:
        query_parts.append(f'(TO "{to}")')
    if from_ is not None:
        query_parts.append(f'(FROM "{from_}")')
    if since_date is not None:
        since_date_str = since_date.strftime("%d-%b-%Y")
        query_parts.append(f'(SINCE "{since_date_str}")')
    if until_date is not None:
        until_date_str = until_date.strftime("%d-%b-%Y")
        query_parts.append(f'(BEFORE "{until_date_str}")')
    if since_emailid is not None:
        query_parts.append(f'(UID {since_emailid}:*)')

    query = ' '.join(query_parts)

    ret = []

    resp, items = self.imap_server.uid('search', None, query)
    items = items[0].split()
    for emailid in items[::-1]:
        resp, data = self.imap_server.uid('fetch', emailid, "(BODY[HEADER.FIELDS (SUBJECT TO FROM DATE)])")
        try:
            raw_email = data[0][1].decode("utf-8")
        except UnicodeDecodeError:
            ValueError(f"Could not decode email with id {emailid}")
        email_message = email.message_from_string(raw_email)

        email_line = {}
        email_line['id'] = emailid
        email_line["to"] = email_message['To']
        email_line["from"] = email_message['From']
        email_line["subject"] = str(email_message['Subject'])
        email_line["created_at"] = email_message['Date']
        resp, email_data = self.imap_server.uid('fetch', emailid, '(BODY[TEXT])')
        email_line["body"] = email_data[0][1].decode('utf-8')

        ret.append(email_line)

    self.imap_server.logout()

    return pd.DataFrame(ret)

Solution

  • I am aware that it is possible to use an app password to make this connection, but is there a way to use the actual email password instead?

    No there is not, you need to do one of two things.

    1. enable 2fa on the account and create an apps password
    2. Use Xoauth2 and request authorization of the user to access their account.

    Your code looks fine to me just crate an apps password.

    Xoauth sample

    from __future__ import print_function
    
    import base64
    import os.path
    import smtplib
    
    from email.mime.text import MIMEText
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials
    from google_auth_oauthlib.flow import InstalledAppFlow
    
    # If modifying these scopes, delete the file token.json.
    SCOPES = ['https://mail.google.com/']
    
    # user token storage
    USER_TOKENS = 'token.json'
    
    # application credentials
    CREDENTIALS = 'C:\YouTube\dev\credentials.json'
    
    
    def getToken() -> str:
        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(USER_TOKENS):
            creds = Credentials.from_authorized_user_file(USER_TOKENS, SCOPES)
            creds.refresh(Request())
        # 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, SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open(USER_TOKENS, 'w') as token:
                token.write(creds.to_json())
    
        return creds.token
    
    
    def generate_oauth2_string(username, access_token) -> str:
        auth_string = 'user=' + username + '\1auth=Bearer ' + access_token + '\1\1'
        return base64.b64encode(auth_string.encode('ascii')).decode('ascii')
    
    def send_email(host, port, subject, msg, sender, recipients):
        access_token = getToken()
        auth_string = generate_oauth2_string(sender, access_token)
    
        msg = MIMEText(msg)
        msg['Subject'] = subject
        msg['From'] = sender
        msg['To'] = ', '.join(recipients)
    
        server = smtplib.SMTP(host, port)
        server.starttls()
        server.docmd('AUTH', 'XOAUTH2 ' + auth_string)
        server.sendmail(sender, recipients, msg.as_string())
        server.quit()
    
    
    def main():
        host = "smtp.gmail.com"
        port = 587
    
        user = "[email protected]"
        recipient = "[email protected]"
        subject = "Test email Oauth2"
        msg = "Hello world"
        sender = user
        recipients = [recipient]
        send_email(host, port, subject, msg, sender, recipients)
    
    
    if __name__ == '__main__':
        main()