Python Forum
Tkinter basic email client
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Tkinter basic email client
#1
This is my first attempt at an email client. It does not handle all email. It's setup for plain text. Will handle some multipart but I have it setup to remove some html tags. Current setting will poll mail server once a minute. Right clicking on the subject will delete. As all my scripts this was done on a linux system so I do not know how it will look on a windows system.
As always I welcome all comments. I tested on my own personal mail server so, I do not know if it works on servers like gmail, yahoo, etc...

#! /usr/bin/env python3
# Do the imports
import imaplib
import email
import tkinter as tk
from functools import partial
import re

# Set mail server connection vars
user = ''
passwd = ''
server = ''

def cleaner(tags):
    cleanr = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
    cleantext = re.sub(cleanr, '', tags)
    return cleantext

class Mail:
    def __init__(self, parent):
        # Configure the root window
        self.parent = parent
        self.parent.columnconfigure(0, weight=1)
        self.parent.rowconfigure(0, weight=1)

        # Get screen width & height
        self.width = self.parent.winfo_screenwidth()
        self.height = self.parent.winfo_screenheight()

        # Setup the mainframe container.
        self.mainframe = tk.Frame(self.parent)
        self.mainframe['width'] = int(self.width/2)
        self.mainframe['height'] = int(self.height/2)
        self.mainframe.grid(column=0, row=0, sticky='new')
        self.mainframe.grid_columnconfigure(0, weight=3)
        self.mainframe.grid_rowconfigure(0, weight=3)

        # Setup header frame to place our widget header
        self.headerframe = tk.Frame(self.mainframe)
        self.headerframe['width'] = int((self.width/2)-1)
        self.headerframe['relief'] = 'ridge'
        self.headerframe['borderwidth'] = 2
        self.headerframe.grid(column=0, row=0, sticky='new')
        self.headerframe.grid_columnconfigure(0, weight=3)

        # Setup the mini header frame. This will hold the labels
        # for the inbox and messages labels
        self.mini_headerframe = tk.Frame(self.mainframe)
        self.mini_headerframe.grid(column=0, row=1, sticky='new', pady=3)
        self.mini_headerframe.grid_columnconfigure(0, weight=1)
        self.mini_headerframe.grid_columnconfigure(1, weight=10)

        # Setup the subject frame
        self.subjectframe = tk.Frame(self.mini_headerframe)
        self.subjectframe['relief'] = 'solid'
        self.subjectframe['borderwidth'] = 1
        self.subjectframe['border'] = 0
        self.subjectframe['highlightthickness'] = 1
        self.subjectframe['highlightbackground'] = 'grey65'
        self.subjectframe.grid(column=0, row=1, sticky='news', pady=2, ipady=2)

        # Message frame
        self.message_frame = tk.Frame(self.mini_headerframe)
        self.message_frame['relief'] = 'solid'
        self.message_frame['borderwidth'] = 1
        self.message_frame['border'] = 0
        self.message_frame['bg'] = 'white'
        self.message_frame['highlightthickness'] = 1
        self.message_frame['highlightbackground'] = 'grey65'
        self.message_frame.grid(column=1, row=1, sticky='news', pady=2, ipady=2)

        #Setup the inbox label
        self.inbox_label = tk.Label(self.mini_headerframe)
        self.inbox_label['text'] = 'Inbox'
        self.inbox_label['font'] = 'sans 10 bold'
        self.inbox_label['relief'] = 'solid'
        self.inbox_label['borderwidth'] = 1
        self.inbox_label['border'] = 0
        self.inbox_label['highlightthickness'] = 1
        self.inbox_label['highlightbackground'] = 'grey65'
        self.inbox_label['width'] = 50
        self.inbox_label.grid(column=0, row=0, sticky='new')

        # Setup the messages label
        self.msg_label = tk.Label(self.mini_headerframe)
        self.msg_label['text'] = 'Messages'
        self.msg_label['font'] = 'sans 10 bold'
        self.msg_label['relief'] = 'solid'
        self.msg_label['borderwidth'] = 1
        self.msg_label['border'] = 0
        self.msg_label['highlightthickness'] = 1
        self.msg_label['highlightbackground'] = 'grey65'
        self.msg_label['width'] = 80
        self.msg_label.grid(column=1, row=0, sticky='new')

        # Setup the header label
        self.header_label = tk.Label(self.headerframe, padx=5, pady=2)
        self.header_label['text'] = 'Tkinter Email Client'
        self.header_label['font'] = 'sans 16 bold'
        self.header_label['fg'] = 'indigo'
        self.header_label.grid(column=0, row=0, sticky='new')
        self.header_label.grid_columnconfigure(0, weight=3)

        self.scrollbar = tk.Scrollbar(self.message_frame)
        self.message = tk.Text(self.message_frame, wrap='word', padx=5, pady=3)
        self.message['width'] = 87
        self.message['height'] = 26
        self.message['state'] = 'disabled'
        self.scrollbar.config(command=self.message.yview)
        self.message.config(yscrollcommand=self.scrollbar.set)
        self.message.insert(tk.END, ' ')
        self.scrollbar.grid(column=0, row=0, sticky='ns')
        self.message.grid(column=1, row=0, sticky='news')


        self.subjects()



    def subjects(self):
        canvas = tk.Canvas(self.subjectframe)
        canvas['height'] = 450
        canvas['bg'] = 'white'
        canvas['width'] = int(self.width/4)
        scrollbar = tk.Scrollbar(self.subjectframe, orient='vertical', command=canvas.yview)
        scrollable_frame = tk.Frame(canvas)
        scrollable_frame.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))
        canvas.create_window((0, 0), window=scrollable_frame, anchor='nw')
        canvas.configure(yscrollcommand=scrollbar.set)
        scrollbar.grid(column=0, row=0, sticky='ns')
        canvas.grid(column=1, row=0, sticky='news')

        mail = Connect()
        mail = mail.connect(user, passwd, server)

        mailbox = GetInbox()
        uids = mailbox.get_uids(mail)

        headers = GrabHeader()
        header = headers.get_headers(mail, uids)
        i = 0
        for heads in header:
            for head, id in heads.items():
                button = tk.Button(scrollable_frame, cursor='hand2', anchor='w', wraplength=430, justify='left')
                button['text'] = head
                button['relief'] = 'flat'
                button['bg'] = 'white'
                button['fg'] = 'blue'
                button['activeforeground'] = 'red'
                button['command'] = partial(self.messagebox, id=id)
                button['width'] = int(self.width/4)
                button.grid(column=0, row=i, sticky='ew')
                button.bind('<Button-3>', partial(self.delete_message, id))
                i += 1
        mail.close()
        mail.logout()
        self.subjectframe.after(60000, self.subjects)

    def messagebox(self, id):
        mail = Connect()
        mail = mail.connect(user, passwd, server)

        mailbox = GetInbox()
        uids = mailbox.get_uids(mail)
        body = GetBody()
        msg = body.get_body(mail, str(id))

        msg = cleaner(msg)

        self.scrollbar = tk.Scrollbar(self.message_frame)
        self.message = tk.Text(self.message_frame, wrap='word', padx=5, pady=3)
        self.message['width'] = 87
        self.message['height'] = 26
        self.scrollbar.config(command=self.message.yview)
        self.message.config(yscrollcommand=self.scrollbar.set)
        self.message['state'] = 'normal'
        self.message.insert(tk.END,msg)
        self.message['state'] = 'disabled'
        self.scrollbar.grid(column=0, row=0, sticky='ns')
        self.message.grid(column=1, row=0, sticky='news')

        mail.close()
        mail.logout()

    def delete_message(self, id, event):
        mail = Connect()
        mail = mail.connect(user, passwd, server)
        mail.select('inbox')
        mail.uid('STORE', str(id), '+FLAGS', '\\Deleted')
        mail.expunge()
        mail.close()
        mail.logout()
        self.subjectframe.after(0, self.subjects)
        self.message['state'] = 'normal'
        self.message.delete(1.0, tk.END)
        self.message['state'] = 'disabled'


# Create the connection class
class Connect:
    # Connect to the server and return
    def connect(self, user, passwd, server):
        mail = imaplib.IMAP4_SSL(server)
        mail.login(user, passwd)
        return mail


# Class for doin operations on the inbox
class GetInbox:
    def get_uids(self, mail):
        mail.select('inbox')
        result, data = mail.uid('search', None, 'ALL')
        return data

# Class for grabbing message headers and uid
class GrabHeader:
    def get_headers(self, mail, uids):
        subjects = []
        for numbers in range(len(uids[0].split())):
            uid = uids[0].split()[numbers]
            result, data = mail.uid('fetch', uid, '(RFC822)')
            raw_data = data[0][1]
            raw_data_string = raw_data.decode('utf-8')
            message = email.message_from_string(raw_data_string)
            subject = str(email.header.make_header(email.header.decode_header(message['subject'])))
            subjects.append({subject:int(uid)})
        return subjects


# Class for retrieving message body
class GetBody:
    def get_body(self, mail, uid):
        result, data = mail.uid('fetch', uid, '(RFC822)')
        raw_data = data[0][1]
        raw_data_string = raw_data.decode('utf-8')
        message = email.message_from_string(raw_data_string)
        for part in message.walk():
            if part.get_content_type() == 'text/plain':
                body = part.get_payload(decode=True)
                return body.decode()
            else:
                continue


def main():
    root = tk.Tk()
    root.title('Tkinter Email Client')
    root['pady'] = 5
    root['padx'] = 10
    root['borderwidth'] = 2
    root.resizable(False, False)
    Mail(root)
    root.mainloop()

if __name__ == '__main__':
    main()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#2
What's the need for the classes Connect, GetBody,GrabHeader and GetInbox? They don't store any state that their methods operate on, so there's not a need to have multiple instances of them (which I guess you realised, since none of those classes have a __init__). Why aren't you just using functions?
Reply
#3
Trying to get in the habit of having classes to do one thing only. Is that not what you said in another post? I could have made one class fits all, in fact that's how I started the code. Just experimenting that's all.
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#4
Well, what I meant was one conceptual thing, not necessarily that the class should have one method. By conceptual thing I mean things like "wrapping up access to the database" (so it might have methods for reading items, as well as writing them) or "displaying items in a window" (which means that it should only be concerned with displaying stuff given to it, not doing any calculations or other logic that aren't its business).

Again, for those 4 cases, the use of classes doesn't make sense. Functions seem to be all you need.
Reply
#5
Also, if I haven't recommended it before, you might want to get a copy of Bob Martin's "Clean Code", in which he discusses issues like this. The example code is in Java, but the points are mostly language independent.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Basic Music Player with tkinter menator01 4 4,656 Jul-31-2021, 04:27 AM
Last Post: ndc85430
  S3Zilla... an S3 File Transfer Client developed with Python3/Tkinter rootVIII 0 1,863 Apr-21-2019, 05:59 AM
Last Post: rootVIII

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020