Home » Tutorials » How to Read Emails in Python

How to Read Emails in Python

Today, we’re diving into an exciting project where we’ll create an email reader using Python! Imagine having a sleek application on your desktop, offering direct access to your emails, bypassing the need to juggle browsers or apps. This tool will let you connect to your email and read your messages right from a window on your desktop.

Let’s get started and make checking emails a bit more exciting!

Table of Contents

Necessary Libraries

For the code to function properly, make sure to install the tkinter and beautifulsoup4 libraries via the terminal or your command prompt by running these commands:

$ pip install tk
$ pip install beautifulsoup4

Imports

import tkinter as tk
from tkinter import messagebox
import imaplib
import email
from email.header import decode_header
import threading

Since we’re building a graphical user interface (GUI), we import the tkinter library. This includes messagebox for displaying errors and information.

Next, imaplib comes into play, allowing access and manipulation of mail over IMAP (Internet Message Access Protocol). This connection to email servers is crucial.

We also add the email library to manage and parse email messages. The decode_header function helps in making email headers readable.

Finally, threading ensures our application performs multiple tasks simultaneously without freezing. This keeps the main window active and responsive.

Email Account Credentials and IMAP Server Configuration

In this Part, we’ve defined the IMAP_SERVER and the port number, allowing access only to Hotmail or Outlook. However, if a user wants to access Yahoo, for example, they would need the corresponding server information. After that, they simply enter their email address and password.

# Email account credentials
EMAIL_ADDRESS = "Enter Your Email Address"  # Update with your Hotmail address
PASSWORD = "Enter-Your-Password"  # Update with your Hotmail password or app password if 2FA is enabled

# IMAP server configuration
IMAP_SERVER = "outlook.office365.com"  # Hotmail IMAP server
IMAP_PORT = 993  # Hotmail IMAP port

Read Emails Functions

Now, let’s define our functions:

Decode Email Subject Header Function

The first one decodes the raw data of the email’s header and subject using two encoding strategies: it first attempts to use the charset information if available; otherwise, it defaults to UTF-8 encoding. Additionally, it employs messagebox.showerror to display an alert in case any issues arise during the decoding process.

def decode_subject_header(header):
   try:
       decoded_header = email.header.decode_header(header)
       subject = ""
       for part, charset in decoded_header:
           if isinstance(part, bytes):
               if charset is None:
                   # If charset is None, assume utf-8 encoding
                   subject += part.decode('utf-8', errors='replace')
               else:
                   subject += part.decode(charset, errors='replace')
           else:
               subject += part
       return subject
   except Exception as e:
       print(f"Error decoding subject header: {e}")
       return "<Decoding Error>"

Fetch Emails Function

The fetch emails function logs into the email account and selects the inbox (as specified in the code, though you can choose other parameters like ‘spam’ if desired). It then retrieves the raw data of the header and subject from either all emails or just the unseen ones, according to the user’s preference. After retrieval, it parses this data to the previously mentioned function for decoding.

def fetch_emails(fetch_all=False):
   def fetch():
       try:
           # Connect to the IMAP server
           mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
           mail.login(EMAIL_ADDRESS, PASSWORD)
           mail.select('inbox')


           # Search for unseen or all emails based on the parameter
           if fetch_all:
               result, data = mail.search(None, 'ALL')
           else:
               result, data = mail.search(None, 'UNSEEN')


           if result == 'OK':
               email_listbox.delete(0, tk.END)  # Clear previous emails
               for num in data[0].split():
                   # Fetch the email headers
                   result, data = mail.fetch(num, '(BODY[HEADER.FIELDS (FROM SUBJECT)])')
                   if result == 'OK':
                       raw_header = data[0][1]
                       msg = email.message_from_bytes(raw_header)
                       subject = msg.get('subject', None)
                       sender = msg.get('from', None)
                       if subject is not None:
                           if isinstance(subject, str):  # Check if subject is a string
                               subject = decode_subject_header(subject)
                           else:
                               subject = str(subject)  # Convert to string if not already
                       else:
                           subject = ""
                       if sender is not None:
                           if isinstance(sender, str):  # Check if sender is a string
                               sender = sender.split()[-1]  # extracting only the email address part
                           else:
                               sender = str(sender)  # Convert to string if not already
                       else:
                           sender = ""
                       email_listbox.insert(tk.END, f"From: {sender}\nSubject: {subject}\n")
                       email_listbox.insert(tk.END, "\n")  # Add empty line for readability


           mail.close()
           mail.logout()
       except Exception as e:
           messagebox.showerror("Error", str(e))


   threading.Thread(target=fetch).start()

Fetch and Display Selected Email Function

This one retrieves the decoded header and subject from the previous functions and displays it on the email_listbox widget.

def fetch_and_display_selected():
   try:
       selected_indices = email_listbox.curselection()
       if selected_indices:  # Check if any item is selected
           selected_index = selected_indices[0]
           email_content_text.delete(1.0, tk.END)
           selected_email = email_listbox.get(selected_index)
           email_content_text.insert(tk.END, selected_email)
           # Fetch the full content of the selected email
           threading.Thread(target=fetch_email_content, args=(selected_index,)).start()
   except IndexError:
       pass

Fetch Email Content Function

The last function is triggered when the user selects a header in the email_listbox widget. It logs into the email address, selects the inbox, retrieves the full content of the selected email, and displays it in the email_content_text widget.

def fetch_email_content(index):
   try:
       mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
       mail.login(EMAIL_ADDRESS, PASSWORD)
       mail.select('inbox')
       result, data = mail.search(None, 'SEEN')
       if result == 'OK':
           num_list = data[0].split()
           print("Number of emails:", len(num_list))  # Debug print
           if index < len(num_list):
               num = num_list[index]
               result, data = mail.fetch(num, '(RFC822)')
               if result == 'OK':
                   raw_email = data[0][1]
                   msg = email.message_from_bytes(raw_email)
                   email_content_text.insert(tk.END, f"\n{'-'*50}\n")
                   for part in msg.walk():
                       content_type = part.get_content_type()
                       content_disposition = str(part.get("Content-Disposition"))
                       if content_type == "text/html":
                           from bs4 import BeautifulSoup
                           soup = BeautifulSoup(part.get_payload(decode=True), "html.parser")
                           for link in soup.find_all('a', href=True):
                               email_content_text.insert(tk.END, f"URL: {link['href']}\n")
                       elif content_type == "text/plain":
                           try:
                               # Attempt to decode the email content using UTF-8
                               decoded_content = part.get_payload(decode=True).decode('utf-8')
                               email_content_text.insert(tk.END, decoded_content + "\n")
                           except UnicodeDecodeError:
                               # If decoding with UTF-8 fails, try Latin-1 encoding
                               decoded_content = part.get_payload(decode=True).decode('latin-1')
                               email_content_text.insert(tk.END, decoded_content + "\n")
                   email_content_text.insert(tk.END, "\n" + "-" * 50 + "\n\n")
       mail.close()
       mail.logout()
   except Exception as e:
       messagebox.showerror("Error", str(e))

Creating the Main Window and Widgets

Following that, we set up the main window, including its title, and create two buttons. The “Fetch Unseen Emails” button calls the fetch_emails() function with fetch_all set to False, retrieving only unseen emails. The “Fetch All Emails” button calls the same function but fetches all emails by setting fetch_all to True.

Additionally, this part introduces two widgets: the scrollable email_listbox, which displays the email headers, and the email_content_text widget, which shows the content of the selected email.

# Create the main window
root = tk.Tk()
root.title("Email Reader - The Pycodes")


# Create widgets
fetch_button = tk.Button(root, text="Fetch Unseen Emails", command=lambda: fetch_emails(fetch_all=False))
fetch_all_button = tk.Button(root, text="Fetch All Emails", command=lambda: fetch_emails(fetch_all=True))
email_listbox = tk.Listbox(root, width=100, height=20)
email_listbox.bind("<<ListboxSelect>>", lambda event: fetch_and_display_selected())
email_scrollbar = tk.Scrollbar(root, orient=tk.VERTICAL, command=email_listbox.yview)
email_listbox.config(yscrollcommand=email_scrollbar.set)
email_content_text = tk.Text(root, width=100, height=20)


# Layout widgets
fetch_button.pack(pady=5)
fetch_all_button.pack(pady=5)
email_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
email_scrollbar.pack(side=tk.LEFT, fill=tk.Y)
email_content_text.pack(pady=10)

Main Loop

Lastly, this section of the code ensures that the main window remains active and responsive to user interactions until it is closed intentionally.

# Start the Tkinter event loop
root.mainloop()

Example

Full Code

import tkinter as tk
from tkinter import messagebox
import imaplib
import email
from email.header import decode_header
import threading


# Email account credentials
EMAIL_ADDRESS = "Enter Your Email Address"  # Update with your Hotmail address
PASSWORD = "Enter-Your-Password"  # Update with your Hotmail password or app password if 2FA is enabled


# IMAP server configuration
IMAP_SERVER = "outlook.office365.com"  # Hotmail IMAP server
IMAP_PORT = 993  # Hotmail IMAP port


def decode_subject_header(header):
   try:
       decoded_header = email.header.decode_header(header)
       subject = ""
       for part, charset in decoded_header:
           if isinstance(part, bytes):
               if charset is None:
                   # If charset is None, assume utf-8 encoding
                   subject += part.decode('utf-8', errors='replace')
               else:
                   subject += part.decode(charset, errors='replace')
           else:
               subject += part
       return subject
   except Exception as e:
       print(f"Error decoding subject header: {e}")
       return "<Decoding Error>"


def fetch_emails(fetch_all=False):
   def fetch():
       try:
           # Connect to the IMAP server
           mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
           mail.login(EMAIL_ADDRESS, PASSWORD)
           mail.select('inbox')


           # Search for unseen or all emails based on the parameter
           if fetch_all:
               result, data = mail.search(None, 'ALL')
           else:
               result, data = mail.search(None, 'UNSEEN')


           if result == 'OK':
               email_listbox.delete(0, tk.END)  # Clear previous emails
               for num in data[0].split():
                   # Fetch the email headers
                   result, data = mail.fetch(num, '(BODY[HEADER.FIELDS (FROM SUBJECT)])')
                   if result == 'OK':
                       raw_header = data[0][1]
                       msg = email.message_from_bytes(raw_header)
                       subject = msg.get('subject', None)
                       sender = msg.get('from', None)
                       if subject is not None:
                           if isinstance(subject, str):  # Check if subject is a string
                               subject = decode_subject_header(subject)
                           else:
                               subject = str(subject)  # Convert to string if not already
                       else:
                           subject = ""
                       if sender is not None:
                           if isinstance(sender, str):  # Check if sender is a string
                               sender = sender.split()[-1]  # extracting only the email address part
                           else:
                               sender = str(sender)  # Convert to string if not already
                       else:
                           sender = ""
                       email_listbox.insert(tk.END, f"From: {sender}\nSubject: {subject}\n")
                       email_listbox.insert(tk.END, "\n")  # Add empty line for readability


           mail.close()
           mail.logout()
       except Exception as e:
           messagebox.showerror("Error", str(e))


   threading.Thread(target=fetch).start()


def fetch_and_display_selected():
   try:
       selected_indices = email_listbox.curselection()
       if selected_indices:  # Check if any item is selected
           selected_index = selected_indices[0]
           email_content_text.delete(1.0, tk.END)
           selected_email = email_listbox.get(selected_index)
           email_content_text.insert(tk.END, selected_email)
           # Fetch the full content of the selected email
           threading.Thread(target=fetch_email_content, args=(selected_index,)).start()
   except IndexError:
       pass


def fetch_email_content(index):
   try:
       mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
       mail.login(EMAIL_ADDRESS, PASSWORD)
       mail.select('inbox')
       result, data = mail.search(None, 'SEEN')
       if result == 'OK':
           num_list = data[0].split()
           print("Number of emails:", len(num_list))  # Debug print
           if index < len(num_list):
               num = num_list[index]
               result, data = mail.fetch(num, '(RFC822)')
               if result == 'OK':
                   raw_email = data[0][1]
                   msg = email.message_from_bytes(raw_email)
                   email_content_text.insert(tk.END, f"\n{'-'*50}\n")
                   for part in msg.walk():
                       content_type = part.get_content_type()
                       content_disposition = str(part.get("Content-Disposition"))
                       if content_type == "text/html":
                           from bs4 import BeautifulSoup
                           soup = BeautifulSoup(part.get_payload(decode=True), "html.parser")
                           for link in soup.find_all('a', href=True):
                               email_content_text.insert(tk.END, f"URL: {link['href']}\n")
                       elif content_type == "text/plain":
                           try:
                               # Attempt to decode the email content using UTF-8
                               decoded_content = part.get_payload(decode=True).decode('utf-8')
                               email_content_text.insert(tk.END, decoded_content + "\n")
                           except UnicodeDecodeError:
                               # If decoding with UTF-8 fails, try Latin-1 encoding
                               decoded_content = part.get_payload(decode=True).decode('latin-1')
                               email_content_text.insert(tk.END, decoded_content + "\n")
                   email_content_text.insert(tk.END, "\n" + "-" * 50 + "\n\n")
       mail.close()
       mail.logout()
   except Exception as e:
       messagebox.showerror("Error", str(e))


# Create the main window
root = tk.Tk()
root.title("Email Reader - The Pycodes")


# Create widgets
fetch_button = tk.Button(root, text="Fetch Unseen Emails", command=lambda: fetch_emails(fetch_all=False))
fetch_all_button = tk.Button(root, text="Fetch All Emails", command=lambda: fetch_emails(fetch_all=True))
email_listbox = tk.Listbox(root, width=100, height=20)
email_listbox.bind("<<ListboxSelect>>", lambda event: fetch_and_display_selected())
email_scrollbar = tk.Scrollbar(root, orient=tk.VERTICAL, command=email_listbox.yview)
email_listbox.config(yscrollcommand=email_scrollbar.set)
email_content_text = tk.Text(root, width=100, height=20)


# Layout widgets
fetch_button.pack(pady=5)
fetch_all_button.pack(pady=5)
email_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
email_scrollbar.pack(side=tk.LEFT, fill=tk.Y)
email_content_text.pack(pady=10)


# Start the Tkinter event loop
root.mainloop()

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top