Read Programming Python Online

Authors: Mark Lutz

Tags: #COMPUTERS / Programming Languages / Python

Programming Python (124 page)

BOOK: Programming Python
3.38Mb size Format: txt, pdf, ePub
ads
Summary: Solutions and workarounds

The
email
package in Python
3.1 provides powerful tools for parsing and composing mails, and can
be used as the basis for full-featured mail clients like those in this
book with just a few workarounds. As you can see, though, it is less
than fully functional today. Because of that, further specializing
code to its current API is perhaps a temporary solution. Short of
writing our own email parser and composer (not a practical option in a
finitely-sized book!), some compromises are in order here. Moreover,
the inherent complexity of Unicode support in
email
places some limits on how much we can
pursue this thread in this book.

In this edition, we will support Unicode encodings of text parts
and headers in messages composed, and respect the Unicode encodings in
text parts and mail headers of messages fetched. To make this work
with the partially crippled
email
package in
Python 3.1
, though,
we’ll apply the following Unicode policies in various email clients in
this book:

  • Use user preferences and defaults for the preparse decoding
    of full mail text fetched and encoding of text payloads
    sent.

  • Use header information, if available, to decode the
    bytes
    payloads returned by
    get_payload
    when text parts must be
    treated as
    str
    text, but use
    binary mode files to finesse the issue in other contexts.

  • Use formats prescribed by email standard to decode and
    encode message headers such as From and Subject if they are not
    simple text.

  • Apply the fix described to work around the message text
    generation issue for binary parts.

  • Special-case construction of text message objects according
    to Unicode types and
    email
    behavior.

These are not necessarily complete solutions. For example, some
of this edition’s email clients allow for Unicode encodings for both
text attachments and mail headers, but they do nothing about encoding
the full text of messages sent beyond the policies inherited from
smtplib
and implement policies that
might be inconvenient in some use cases. But as we’ll see, despite
their limitations, our email clients will still be able to handle
complex email tasks and a very large set of emails.

Again, since this story is in flux in Python today, watch this
book’s website for updates that may improve or be required of code
that uses
email
in the future. A
future
email
may handle Unicode
encodings more accurately. Like Python 3.X, though, backward
compatibility may be sacrificed in the process and require updates to
this book’s code. For more on this issue, see the Web as well as
up-to-date Python release notes.

Although this quick tour captures the basic flavor of the
interface, we need to step up to larger examples to see more of the
email
package’s power. The next
section takes us on the first of those
steps.

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 13-20
implements an interactive
email session—users may type commands to read, send, and delete email
messages. It uses
poplib
and
smtplib
to
fetch and send, and uses the
email
package directly to parse and
compose.

Example 13-20. PP4E\Internet\Email\pymail.py

#!/usr/local/bin/python
"""
##########################################################################
pymail - a simple console email interface client in Python; uses Python
poplib module to view POP email messages, smtplib to send new mails, and
the email package to extract mail headers and payload and compose mails;
##########################################################################
"""
import poplib, smtplib, email.utils, mailconfig
from email.parser import Parser
from email.message import Message
fetchEncoding = mailconfig.fetchEncoding
def decodeToUnicode(messageBytes, fetchEncoding=fetchEncoding):
"""
4E, Py3.1: decode fetched bytes to str Unicode string for display or parsing;
use global setting (or by platform default, hdrs inspection, intelligent guess);
in Python 3.2/3.3, this step may not be required: if so, return message intact;
"""
return [line.decode(fetchEncoding) for line in messageBytes]
def splitaddrs(field):
"""
4E: split address list on commas, allowing for commas in name parts
"""
pairs = email.utils.getaddresses([field]) # [(name,addr)]
return [email.utils.formataddr(pair) for pair in pairs] # [name ]
def inputmessage():
import sys
From = input('From? ').strip()
To = input('To? ').strip() # datetime hdr may be set auto
To = splitaddrs(To) # possible many, name+ okay
Subj = input('Subj? ').strip() # don't split blindly on ',' or ';'
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) # join for hdr, not send
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 = [] # fetch mail now
for i in range(loadfrom, msgCount+1): # empty if low >= high
(hdr, message, octets) = server.retr(i) # save text on list
message = decodeToUnicode(message) # 4E, Py3.1: bytes to str
msgList.append('\n'.join(message)) # leave mail on server
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=True):
print('To be deleted:', toDelete)
if verify and 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) # expects str in 3.1
count += 1
print('%d:\t%d bytes' % (count, len(msgtext)))
for hdr in ('From', 'To', 'Date', 'Subject'):
try:
print('\t%-8s=>%s' % (hdr, msghdrs[hdr]))
except KeyError:
print('\t%-8s=>(unknown)' % hdr)
if count % 5 == 0:
input('[Press Enter key]') # pause after each 5
def showmessage(i, msgList):
if 1 <= i <= len(msgList):
#print(msgList[i-1]) # old: prints entire mail--hdrs+text
print('-' * 79)
msg = Parser().parsestr(msgList[i-1]) # expects str in 3.1
content = msg.get_payload() # prints payload: string, or [Messages]
if isinstance(content, str): # keep just one end-line at end
content = content.rstrip() + '\n'
print(content)
print('-' * 79) # to get text only, see email.parsers
else:
print('Bad message number')
def savemessage(i, mailfile, msgList):
if 1 <= i <= len(msgList):
savefile = open(mailfile, 'a', encoding=mailconfig.fetchEncoding) # 4E
savefile.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 True:
try:
command = input('[Pymail] Action? (i, l, d, s, m, q, ?) ')
except EOFError:
command = 'q'
if not command: command = '*'
# quit
if 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: # delete all later
toDelete = list(range(1, len(msgList)+1)) # 3.x requires list
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: 'pop.rmi.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 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 of
Example 13-17
.

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 can be applied to the wrong mail if deletions
were done immediately.
[
53
]

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 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 we wrote earlier (
Example 13-17
).

Here it is in action running on my Windows laptop machine; its
operation is identical on other machines thanks to the portability of
both Python and its standard library. First, we start the script, supply
a POP password (remember, SMTP servers usually 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:\...\PP4E\Internet\Email>
pymail.py
Password for pop.secureserver.net?
[Pymail email client]
Connecting...
b'+OK <[email protected]>'
(b'+OK ', [b'1 1860', b'2 1408', b'3 1049', b'4 1009', b'5 1038', b'6 957'], 47)
There are 6 mail messages in 7321 bytes
Retrieving...
1: 1861 bytes
From =>[email protected]
To =>[email protected]
Date =>Wed, 5 May 2010 11:29:36 −0400 (EDT)
Subject =>I'm a Lumberjack, and I'm Okay
2: 1409 bytes
From =>[email protected]
To =>[email protected]
Date =>Wed, 05 May 2010 08:33:47 −0700
Subject =>testing
3: 1050 bytes
From =>[email protected]
To =>[email protected]
Date =>Thu, 06 May 2010 14:11:07 −0000
Subject =>A B C D E F G
4: 1010 bytes
From =>[email protected]
To =>[email protected]
Date =>Thu, 06 May 2010 14:16:31 −0000
Subject =>testing smtpmail
5: 1039 bytes
From =>[email protected]
To =>[email protected]
Date =>Thu, 06 May 2010 14:32:32 −0000
Subject =>a b c d e f g
[Press Enter key]
6: 958 bytes
From =>[email protected]
To =>maillist
Date =>Thu, 06 May 2010 10:58:40 −0400
Subject =>test interactive smtplib
[Pymail] Action? (i, l, d, s, m, q, ?)
l 6
-------------------------------------------------------------------------------
testing 1 2 3...
-------------------------------------------------------------------------------
[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 just used
it to list two emails we sent in the preceding section, with the
smtpmail
script, and
interactively.

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, ?)
s 4
[Pymail] Action? (i, l, d, s, m, q, ?)
d 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
. You
can separate recipients with a comma, and use either simple “addr” or
full “name ” address pairs if desired. 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: [4]
Delete?
y
Connecting...
b'+OK <[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
(we’ll improve on this in the PyMailGUI client of the next
chapter).

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 at the end as
new number 6, and the original mail assigned number 4 in the prior
session is gone:

C:\...\PP4E\Internet\Email>
pymail.py
Password for pop.secureserver.net?
[Pymail email client]
Connecting...
b'+OK <[email protected]>'
(b'+OK ', [b'1 1860', b'2 1408', b'3 1049', b'4 1038', b'5 957', b'6 1037'], 47)
There are 6 mail messages in 7349 bytes
Retrieving...
1: 1861 bytes
From =>[email protected]
To =>[email protected]
Date =>Wed, 5 May 2010 11:29:36 −0400 (EDT)
Subject =>I'm a Lumberjack, and I'm Okay
2: 1409 bytes
From =>[email protected]
To =>[email protected]
Date =>Wed, 05 May 2010 08:33:47 −0700
Subject =>testing
3: 1050 bytes
From =>[email protected]
To =>[email protected]
Date =>Thu, 06 May 2010 14:11:07 −0000
Subject =>A B C D E F G
4: 1039 bytes
From =>[email protected]
To =>[email protected]
Date =>Thu, 06 May 2010 14:32:32 −0000
Subject =>a b c d e f g
5: 958 bytes
From =>[email protected]
To =>maillist
Date =>Thu, 06 May 2010 10:58:40 −0400
Subject =>test interactive smtplib
[Press Enter key]
6: 1038 bytes
From =>[email protected]
To =>[email protected]
Date =>Fri, 07 May 2010 20:32:38 −0000
Subject =>Among our weapons are these
[Pymail] Action? (i, l, d, s, m, q, ?)
l 6
-------------------------------------------------------------------------------
Nobody Expects the Spanish Inquisition!
-------------------------------------------------------------------------------
[Pymail] Action? (i, l, d, s, m, q, ?)
q
Bye.

Though not shown in this session, you can also send to multiple
recipients, and include full name and address pairs in your email
addresses. This works just because the script employs
email
utilities described earlier to split up
addresses and fully parse to allow commas as both separators and name
characters. The following, for example, would send to two and three
recipients, respectively, using mostly full address formats:

[Pymail] Action? (i, l, d, s, m, q, ?)
m
From?
"moi 1"
To?
"pp 4e" , "lu,tz"
[Pymail] Action? (i, l, d, s, m, q, ?)
m
From?
The Book
To?
"pp 4e" , "lu,tz" ,
[email protected]

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. As shown in this
book, it shows up in file
C:\temp\savemail.txt
, but you can configure
this as you like in the
mailconfig
module.

BOOK: Programming Python
3.38Mb size Format: txt, pdf, ePub
ads

Other books

All Night Long by Candace Schuler
Pee Wee Pool Party by Judy Delton
Her Lone Cowboy by Donna Alward
Lost and Found in Cedar Cove by Debbie Macomber
Love Not a Rebel by Heather Graham
Sleeper Seven by Mark Howard