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 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:

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

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.

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:...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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.186.178