Let’s put together what we’ve learned about fetching, sending, parsing, and composing email in a simple but functional command-line console email tool. The script in Example 14-20 implements an interactive email session—users may type commands to read, send, and delete email messages.
Example 14-20. PP3EInternetEmailpymail.py
#!/usr/local/bin/python ########################################################################## # pymail - a simple console email interface client in Python; uses Python # POP3 mail interface module to view POP email account messages; uses # email package modules to extract mail message headers (not rfc822); ########################################################################## import poplib, smtplib, email.Utils from email.Parser import Parser from email.Message import Message def inputmessage( ): import sys From = raw_input('From? ').strip( ) To = raw_input('To? ').strip( ) # datetime hdr set auto To = To.split(';') Subj = raw_input('Subj? ').strip( ) print 'Type message text, end with line="."' text = '' while True: line = sys.stdin.readline( ) if line == '. ': break text += line return From, To, Subj, text def sendmessage( ): From, To, Subj, text = inputmessage( ) msg = Message( ) msg['From'] = From msg['To'] = ';'.join(To) msg['Subject'] = Subj msg['Date'] = email.Utils.formatdate( ) # curr datetime, rfc2822 msg.set_payload(text) server = smtplib.SMTP(mailconfig.smtpservername) try: failed = server.sendmail(From, To, str(msg)) # may also raise exc except: print 'Error - send failed' else: if failed: print 'Failed:', failed def connect(servername, user, passwd): print 'Connecting...' server = poplib.POP3(servername) server.user(user) # connect, log in to mail server server.pass_(passwd) # pass is a reserved word print server.getwelcome( ) # print returned greeting message return server def loadmessages(servername, user, passwd, loadfrom=1): server = connect(servername, user, passwd) try: print server.list( ) (msgCount, msgBytes) = server.stat( ) print 'There are', msgCount, 'mail messages in', msgBytes, 'bytes' print 'Retrieving:', msgList = [] for i in range(loadfrom, msgCount+1): # empty if low >= high print i, # fetch mail now (hdr, message, octets) = server.retr(i) # save text on list msgList.append(' '.join(message)) # leave mail on server print finally: server.quit( ) # unlock the mail box assert len(msgList) == (msgCount - loadfrom) + 1 # msg nums start at 1 return msgList def deletemessages(servername, user, passwd, toDelete, verify=1): print 'To be deleted:', toDelete if verify and raw_input('Delete?')[:1] not in ['y', 'Y']: print 'Delete cancelled.' else: server = connect(servername, user, passwd) try: print 'Deleting messages from server.' for msgnum in toDelete: # reconnect to delete mail server.dele(msgnum) # mbox locked until quit( ) finally: server.quit( ) def showindex(msgList): count = 0 # show some mail headers for msgtext in msgList: msghdrs = Parser( ).parsestr(msgtext, headersonly=True) count = count + 1 print '%d: %d bytes' % (count, len(msgtext)) for hdr in ('From', 'Date', 'Subject'): try: print ' %s=>%s' % (hdr, msghdrs[hdr]) except KeyError: print ' %s=>(unknown)' % hdr #print ' %s=>%s' % (hdr, msghdrs.get(hdr, '(unknown)') if count % 5 == 0: raw_input('[Press Enter key]') # pause after each 5 def showmessage(i, msgList): if 1 <= i <= len(msgList): print '-'*80 msg = Parser( ).parsestr(msgList[i-1]) print msg.get_payload( ) # prints payload: string, or [Messages] #print msgList[i-1] # old: prints entire mail--hdrs+text print '-'*80 # to get text only, call file.read( ) else: # after rfc822.Message reads hdr lines print 'Bad message number' def savemessage(i, mailfile, msgList): if 1 <= i <= len(msgList): open(mailfile, 'a').write(' ' + msgList[i-1] + '-'*80 + ' ') else: print 'Bad message number' def msgnum(command): try: return int(command.split( )[1]) except: return -1 # assume this is bad helptext = """ Available commands: i - index display l n? - list all messages (or just message n) d n? - mark all messages for deletion (or just message n) s n? - save all messages to a file (or just message n) m - compose and send a new mail message q - quit pymail ? - display this help text """ def interact(msgList, mailfile): showindex(msgList) toDelete = [] while 1: try: command = raw_input('[Pymail] Action? (i, l, d, s, m, q, ?) ') except EOFError: command = 'q' # quit if not command or command == 'q': break # index elif command[0] == 'i': showindex(msgList) # list elif command[0] == 'l': if len(command) == 1: for i in range(1, len(msgList)+1): showmessage(i, msgList) else: showmessage(msgnum(command), msgList) # save elif command[0] == 's': if len(command) == 1: for i in range(1, len(msgList)+1): savemessage(i, mailfile, msgList) else: savemessage(msgnum(command), mailfile, msgList) # delete elif command[0] == 'd': if len(command) == 1: toDelete = range(1, len(msgList)+1) # delete all later else: delnum = msgnum(command) if (1 <= delnum <= len(msgList)) and (delnum not in toDelete): toDelete.append(delnum) else: print 'Bad message number' # mail elif command[0] == 'm': # send a new mail via SMTP sendmessage( ) #execfile('smtpmail.py', {}) # alt: run file in own namespace elif command[0] == '?': print helptext else: print 'What? -- type "?" for commands help' return toDelete if _ _name_ _ == '_ _main_ _': import getpass, mailconfig mailserver = mailconfig.popservername # ex: 'starship.python.net' mailuser = mailconfig.popusername # ex: 'lutz' mailfile = mailconfig.savemailfile # ex: r'c:stuffsavemail' mailpswd = getpass.getpass('Password for %s?' % mailserver) print '[Pymail email client]' msgList = loadmessages(mailserver, mailuser, mailpswd) # load all toDelete = interact(msgList, mailfile) if toDelete: deletemessages(mailserver, mailuser, mailpswd, toDelete) print 'Bye.'
There isn’t much new here—just a combination of user-interface logic and tools we’ve already met, plus a handful of new techniques:
This client loads all email from the server into an in-memory Python list only once, on startup; you must exit and restart to reload newly arrived email.
On demand, pymail
saves
the raw text of a selected message into a local file, whose name
you place in the mailconfig
module.
We finally support on-request deletion of mail from the
server here: in pymail
, mails
are selected for deletion by number, but are still only
physically removed from your server on exit, and then only if
you verify the operation. By deleting only on exit, we avoid changing mail message
numbers during a session—under POP, deleting a mail not at the
end of the list decrements the number assigned to all mails
following the one deleted. Since mail is cached in memory by
pymail
, future operations on
the numbered messages in memory may be applied to the wrong mail
if deletions were done immediately.[*]
pymail
now displays
just the payload of a message on listing commands, not the
entire raw text, and the mail index listing only displays
selected headers parsed out of each message. Python’s email
package is used to extract
headers and content from a message, as shown in the prior
section. Similarly, we use email
to compose a message and ask for
its string to ship as a mail.
By now, I expect that you know enough Python to read this script
for a deeper look, so instead of saying more about its design here,
let’s jump into an interactive pymail
session to see how it works.
Let’s start up pymail
to read and delete email at our
mail server and send new messages. pymail
runs on any machine with Python and
sockets, fetches mail from any email server with a POP interface on
which you have an account, and sends mail via the SMTP server you’ve
named in the mailconfig
module.
Here it is in action running on my Windows laptop machine; its
operation is identical on other machines. First, we start the
script, supply a POP password (remember, SMTP servers require no
password), and wait for the pymail
email list index to appear; as is,
this version loads the full text of all mails in the inbox on
startup:
C:...PP3EInternetEmail>python pymail.py
Password for pop.earthlink.net? [Pymail email client] Connecting... +OK NGPopper vEL_6_10 at earthlink.net ready <[email protected]... ('+OK', ['1 876', '2 800', '3 818', '4 770', '5 819'], 35) There are 5 mail messages in 4083 bytes Retrieving: 1 2 3 4 5 1: 1019 bytes From=>[email protected] Date=>Wed, 08 Feb 2006 05:23:13 -0000 Subject=>I'm a Lumberjack, and I'm Okay 2: 883 bytes From=>[email protected] Date=>Wed, 08 Feb 2006 05:24:06 -0000 Subject=>testing 3: 967 bytes From=>[email protected] Date=>Tue Feb 07 22:51:08 2006 Subject=>A B C D E F G 4: 854 bytes From=>[email protected] Date=>Tue Feb 07 23:19:51 2006 Subject=>testing smtpmail 5: 968 bytes From=>[email protected] Date=>Tue Feb 07 23:34:23 2006 Subject=>a b c d e f g [Press Enter key] [Pymail] Action? (i, l, d, s, m, q, ?)l 5
-------------------------------------------------------------------------------- Spam; Spam and eggs; Spam, spam, and spam -------------------------------------------------------------------------------- [Pymail] Action? (i, l, d, s, m, q, ?)l 3
-------------------------------------------------------------------------------- Fiddle de dum, Fiddle de dee, Eric the half a bee. -------------------------------------------------------------------------------- [Pymail] Action? (i, l, d, s, m, q, ?)
Once pymail
downloads your
email to a Python list on the local client machine, you type command
letters to process it. The l
command lists (prints) the contents of a given mail number; here, we
used it to list the two emails we wrote with the smtpmail
script in the preceding
section.
pymail
also lets us get
command help, delete messages (deletions actually occur at the
server on exit from the program), and save messages away in a local
text file whose name is listed in the mailconfig
module we saw earlier:
[Pymail] Action? (i, l, d, s, m, q, ?)?
Available commands: i - index display l n? - list all messages (or just message n) d n? - mark all messages for deletion (or just message n) s n? - save all messages to a file (or just message n) m - compose and send a new mail message q - quit pymail ? - display this help text [Pymail] Action? (i, l, d, s, m, q, ?)d 1
[Pymail] Action? (i, l, d, s, m, q, ?)s 4
Now, let’s pick the m
mail
compose option—pymail
inputs the
mail parts, builds mail text with email
, and ships it off with smtplib
. Because the mail is sent by SMTP,
you can use arbitrary “From” addresses here; but again, you
generally shouldn’t do that (unless, of course, you’re trying to
come up with interesting examples for a book):
[Pymail] Action? (i, l, d, s, m, q, ?)m
From?[email protected]
To?[email protected]
Subj?Among our weapons are these:
Type message text, end with line="."Nobody Expects the Spanish Inquisition!
.
[Pymail] Action? (i, l, d, s, m, q, ?)q
To be deleted: [1] Delete?y
Connecting... +OK NGPopper vEL_6_10 at earthlink.net ready <[email protected]... Deleting messages from server. Bye.
As mentioned, deletions really happen only on exit. When we
quit pymail
with the q
command, it tells us which messages are
queued for deletion, and verifies the request. Once verified,
pymail
finally contacts the mail
server again and issues POP calls to delete the selected mail
messages. Because deletions change message numbers in the server’s
inbox, postponing deletion until exit simplifies the handling of
already loaded email.
Because pymail
downloads
mail from your server into a local Python list only once at startup,
though, we need to start pymail
again to refetch mail from the server if we want to see the result
of the mail we sent and the deletion we made. Here, our new mail
shows up as number 5, and the original mail assigned number 1 is
gone:
C:...PP3EInternetEmail>pymail.py
Password for pop.earthlink.net? [Pymail email client] Connecting... +OK NGPopper vEL_6_10 at earthlink.net ready <[email protected]... ('+OK', ['1 800', '2 818', '3 770', '4 819', '5 841'], 35) There are 5 mail messages in 4048 bytes Retrieving: 1 2 3 4 5 1: 883 bytes From=>[email protected] Date=>Wed, 08 Feb 2006 05:24:06 -0000 Subject=>testing 2: 967 bytes From=>[email protected] Date=>Tue Feb 07 22:51:08 2006 Subject=>A B C D E F G 3: 854 bytes From=>[email protected] Date=>Tue Feb 07 23:19:51 2006 Subject=>testing smtpmail 4: 968 bytes From=>[email protected] Date=>Tue Feb 07 23:34:23 2006 Subject=>a b c d e f g 5: 989 bytes From=>[email protected] Date=>Wed, 08 Feb 2006 08:58:27 -0000 Subject=>Among our weapons are these: [Press Enter key] [Pymail] Action? (i, l, d, s, m, q, ?)l 5
-------------------------------------------------------------------------------- Nobody Expects the Spanish Inquisition! -------------------------------------------------------------------------------- [Pymail] Action? (i, l, d, s, m, q, ?)q
Bye.
Finally, if you are running this live, you will also find the
mail save file on your machine, containing the one message we asked
to be saved in the prior session; it’s simply the raw text of saved
emails, with separator lines. This is both human and
machine-readable—in principle, another script could load saved mail
from this file into a Python list by calling the string object’s
split
method on the file’s text
with the separator line as a delimiter.
[*] There will be more on POP message numbers when we
study mailtools
later in
this chapter. Interestingly, the list of message numbers to
be deleted need not be sorted; they remain valid for the
duration of the delete connection, so deletions earlier in
the list don’t change numbers of messages later in the list
while you are still connected to the POP server. We’ll also
see that some subtle issues may arise if mails in the server
inbox are deleted without pymail
’s knowledge (e.g., by your
ISP or another email client); although very rare, suffice it to
say for now that deletions in this script are not guaranteed
to be accurate.
18.116.65.130