Welcome, guest | Sign In | My Account | Store | Cart

Polls an IMAP inbox for unread messages and displays the sender and subject in a scrollable window using Tkinter.

Reads servername, user, and password from ~/.imap file. They must be on one line, separated with spaces.

Python, 84 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python
import imaplib, string, sys, os, re, rfc822
from Tkinter import *

PollInterval = 60 # seconds

def getimapaccount():
    try:
        f = open(os.path.expanduser('~/.imap'))
    except IOError, e:
        print 'Unable to open ~/.imap: ', e
        sys.exit(1)
    global imap_server, imap_user, imap_password
    try:
        imap_server, imap_user, imap_password = string.split(f.readline())
    except ValueError:
        print 'Invalid data in ~/.imap'
        sys.exit(1)
    f.close()

class msg: # a file-like object for passing a string to rfc822.Message
    def __init__(self, text):
	self.lines = string.split(text, '\015\012')
	self.lines.reverse()
    def readline(self):
	try: return self.lines.pop() + '\n'
	except: return ''

class Mailwatcher(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master)
        self.pack(side=TOP, expand=YES, fill=BOTH)
        self.scroll = Scrollbar(self)
        self.list = Listbox(self, font='7x13',
                            yscrollcommand=self.scroll.set,
                            setgrid=1, height=6, width=80)
        self.scroll.configure(command=self.list.yview)
        self.scroll.pack(side=LEFT, fill=BOTH)
        self.list.pack(side=LEFT, expand=YES, fill=BOTH)

    def getmail(self):
	self.after(1000*PollInterval, self.getmail)
        self.list.delete(0,END)
	try:
	    M = imaplib.IMAP4(imap_server)
	    M.login(imap_user, imap_password)
	except Exception, e:
	    self.list.insert(END, 'IMAP login error: ', e)
	    return

	try:
	    result, message = M.select(readonly=1)
	    if result != 'OK':
		raise Exception, message
	    typ, data = M.search(None, '(UNSEEN UNDELETED)')
	    for num in string.split(data[0]):
		try:
		    f = M.fetch(num, '(BODY[HEADER.FIELDS (SUBJECT FROM)])')
		    m = rfc822.Message(msg(f[1][0][1]), 0)
		    subject = m['subject']
		except KeyError:
		    f = M.fetch(num, '(BODY[HEADER.FIELDS (FROM)])')
		    m = rfc822.Message(msg(f[1][0][1]), 0)
		    subject = '(no subject)'
		fromaddr = m.getaddr('from')
		if fromaddr[0] == "": n = fromaddr[1]
		else: n = fromaddr[0]
		text = '%-20.20s  %s' % (n, subject)
		self.list.insert(END, text)
	    len = self.list.size()
	    if len > 0: self.list.see(len-1)
	except Exception, e:
	    self.list.delete(0,END)
	    print sys.exc_info()
	    self.list.insert(END, 'IMAP read error: ', e)
	M.logout()


getimapaccount()
root = Tk(className='mailwatcher')
root.title('mailwatcher')
mw = Mailwatcher(root)
mw.getmail()
mw.mainloop()

Figuring out how to get the IMAP part working took a fair bit of investigating. The best way to debug it is to talk to the IMAP server directly from the python console and see what it returns:

>>> import imaplib
>>> M = imaplib.IMAP4(imap_server)
>>> M.login(imap_user, imap_password)
('OK', ['LOGIN complete'])
>>> M.select(readonly=1)
('OK', ['8'])
>>> M.search(None, '(UNSEEN UNDELETED)')
('OK', ['8'])
>>> M.fetch(8, '(BODY[HEADER.FIELDS (SUBJECT FROM)])')
('OK', [('8 (BODY[HEADER.FIELDS (SUBJECT FROM)] {71}', 'From: John Doe <John.Doe@nowhere.com>\015\012Subject: test message\015\012\015\012'), ')'])

2 comments

David Blewett 17 years, 6 months ago  # | flag

Improvement. Thanks for taking the time to contribute this! It really helped me learn how to interface with imaplib. I have a couple of improvements that can reduce the network traffic of this script.

First of all, this bit:

typ, data = M.search(None, '(UNSEEN UNDELETED)')
    for num in string.split(data[0]):
        try:
            f = M.fetch(num, '(BODY[HEADER.FIELDS (SUBJECT FROM)])')
            m = rfc822.Message(msg(f[1][0][1]), 0)

can be updated to this:

typ, data = M.search(None, '(UNSEEN UNDELETED)')
msg_id = [m for m in data[0].split()]
msg_id = ','.join(msg_list)
msg_list = M.fetch(msg_id, '(BODY[HEADER.FIELDS (SUBJECT FROM)])')
for m in msg_list:
    msg = rfc822.Message(msg(m[1][0][1]), 0)

This way, a list of the requested message parts is returned at once instead of hitting the server for each message individually. While we're at it, that last line can be updated to use the email module, like so:

from email.Parser import HeaderParser

for m in msg_list:
    hp = HeaderParser()
    msg = hp.parsestr(m[1][0][1])
sasa sasa 17 years, 5 months ago  # | flag

rough edges. The code is organized in an awkward way.

Presentation does not cleanly separate from the core code.

This showed painfully when I wanted to deploy this recipe on a box where tcl/Tk was not available. Thus I wanted to replace Tkinter calls so that simply an external program gets called if there is a new mail.

Ideally, this should be the matter of replacing a presentation class or function call with another one, leaving IMAP related code intact.

But the Mailwatcher class just blends everything together with no concern, so I had to actually understand the IMAP code to achieve what I want, and alltogether, work more on it.