Section 14.7. pymail: A Console-Based Email Client


14.7. pymail: A Console-Based Email Client

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 sessionusers may type commands to read, send, and delete email messages.

Example 14-20. PP3E\Internet\Email\pymail.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 == '.\n': 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('\n'.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:\t%d bytes' % (count, len(msgtext))         for hdr in ('From', 'Date', 'Subject'):             try:                 print '\t%s=>%s' % (hdr, msghdrs[hdr])             except KeyError:                 print '\t%s=>(unknown)' % hdr             #print '\n\t%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('\n' + msgList[i-1] + '-'*80 + '\n')     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:\stuff\savemail'     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 herejust a combination of user-interface logic and tools we've already met, plus a handful of new techniques:


Loads

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.


Saves

On demand, pymail saves the raw text of a selected message into a local file, whose name you place in the mailconfig module.


Deletions

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 sessionunder 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.[*]

[*] 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.


Parsing and composing messages

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.

14.7.1. Running the pymail Console Client

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:\...\PP3E\Internet\Email>python pymail.py Password for pop.earthlink.net? [Pymail email client] Connecting... +OK NGPopper vEL_6_10 at earthlink.net ready <27597.1139388190@pop-scotia.atl... ('+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=>lumber.jack@TheLarch.com         Date=>Wed, 08 Feb 2006 05:23:13 -0000         Subject=>I'm a Lumberjack, and I'm Okay 2:      883 bytes         From=>pp3e@earthlink.net         Date=>Wed, 08 Feb 2006 05:24:06 -0000         Subject=>testing 3:      967 bytes         From=>Eric.the.Half.a.Bee@yahoo.com         Date=>Tue Feb 07 22:51:08 2006         Subject=>A B C D E F G 4:      854 bytes         From=>pp3e@earthlink.net         Date=>Tue Feb 07 23:19:51 2006         Subject=>testing smtpmail 5:      968 bytes         From=>Eric.the.Half.a.Bee@aol.com         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 optionpymail 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? Cardinal@hotmail.com To?   pp3e@earthlink.net 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 <4603.1139389143@pop-satin.atl... 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:\...\PP3E\Internet\Email>pymail.py Password for pop.earthlink.net? [Pymail email client] Connecting... +OK NGPopper vEL_6_10 at earthlink.net ready <29675.1139389310@pop-satin.atl... ('+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=>pp3e@earthlink.net         Date=>Wed, 08 Feb 2006 05:24:06 -0000         Subject=>testing 2:      967 bytes         From=>Eric.the.Half.a.Bee@yahoo.com         Date=>Tue Feb 07 22:51:08 2006         Subject=>A B C D E F G 3:      854 bytes         From=>pp3e@earthlink.net         Date=>Tue Feb 07 23:19:51 2006         Subject=>testing smtpmail 4:      968 bytes         From=>Eric.the.Half.a.Bee@aol.com         Date=>Tue Feb 07 23:34:23 2006         Subject=>a b c d e f g 5:      989 bytes         From=>Cardinal@hotmail.com         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-readablein 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.




Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net