To display email, PyMailGUI inserts its text into an attachedTextEditor
object; to compose email,
PyMailGUI presents aTextEditor
and
later fetches all its text to ship over the Net. Besides the obvious
simplification here, this code reuse makes it easy to pick up
improvements and fixes—any changes in theTextEditor
object are automatically inherited
by PyMailGUI, PyView, and PyEdit.
In the third edition’s version, for instance, PyMailGUI supports
edit undo and redo, just because PyEdit had gained that feature. And in
this fourth edition, all PyEdit importers also inherit its new Grep file
search, as well as its new support for viewing and editing text of
arbitrary Unicode encodings—especially useful for text parts in emails
of arbitrary origin like those displayed here (see
Chapter 11
for more about PyEdit’s
evolution).
Next, let’s go
back to the PyMailGUI main server list window, and click
the Load button to retrieve incoming email over the POP protocol.
PyMailGUI’s load function gets
account
parameters from themailconfig
module
listed later in this chapter, so be sure to change this file to reflect
your email account parameters (i.e., server names and usernames) if you
wish to use PyMailGUI to read your own email. Unless you can guess the
book’s email account password, the presets in this file won’t work for
you.
The account password parameter merits a few extra words. In
PyMailGUI, it may come from one of two places:
If you put the name of a local file containing the password
in themailconfig
module,
PyMailGUI loads the password from that file as needed.
If you don’t put a password filename inmailconfig
(or if PyMailGUI can’t load
it from the file for whatever reason), PyMailGUI will instead ask
you for your password anytime it is needed.
Figure 14-7
shows the
password input prompt you get if you haven’t stored your password in a
local file. Note that the password you type is not shown—ashow='*'
option for theEntry
field used in this pop up tells tkinter
to echo typed characters as stars (this option is similar in spirit to
both thegetpass
console input module
we met earlier in the prior chapter and an HTMLtype=password
option we’ll meet in a later
chapter). Once entered, the password lives only in memory on your
machine; PyMailGUI itself doesn’t store it anywhere in a permanent
way.
Figure 14-7. PyMailGUI password input dialog
Also notice that the local file password option requires you to
store your password unencrypted in a file on the local client computer.
This is convenient (you don’t need to retype a password every time you
check email), but it is not generally a good idea on a machine you share
with others, of course; leave this setting blank inmailconfig
if you prefer to always enter your
password in a pop up.
Once PyMailGUI fetches your mail parameters and somehow obtains
your password, it will next attempt to pull down just the header text of
all your incoming email from your inbox on your POP email server. On
subsequent loads, only newly arrived mails are loaded, if any. To
support obscenely large inboxes (like one of mine), the program is also
now clever enough to skip fetching headers for all but the last batch of
messages, whose size you can configure inmailconfig
—they show up early in the mail list
with subject line “--mail skipped--”; see the 3.0 changes overview
earlier for more details.
To save time, PyMailGUI fetches message header text only to
populate the list window. The full text of messages is fetched later
only when a message is selected for viewing or processing, and then only
if the full text has not yet been fetched during this session. PyMailGUI
reuses the load-mail tools in themailtools
module of
Chapter 13
to fetch message header text, which
in turn uses Python’s standardpoplib
module to retrieve your email.
Now that we’re
downloading mails, I need to explain the juggling act that
PyMailGUI performs to avoid becoming blocked and support operations that
overlap in time. Ultimately, mail fetches run over sockets on relatively
slow networks. While the download is in progress, the rest of the GUI
remains active—you may compose and send other mails at the same time,
for instance. To show its progress, the nonblocking dialog of
Figure 14-8
is displayed when
the mail index is being fetched.
Figure 14-8. Nonblocking progress indicator: Load
In general, all server transfers display such dialogs.
Figure 14-9
shows the busy
dialog displayed while a full text download of five selected and
uncached (not yet fetched) mails is in progress, in response to a View
action. After this download finishes, all five pop up in individual view
windows.
Figure 14-9. Nonblocking progress indicator: View
Such server transfers, and other long-running operations, are run
in
threads
to avoid blocking the GUI. They do not
disable other actions from running in parallel, as long as those actions
would not conflict with a currently running thread. Multiple mail sends
and disjoint fetches can overlap in time, for instance, and can run in
parallel with the GUI itself—the GUI responds to moves, redraws, and
resizes during the transfers. Other transfers such as mail deletes must
run all by themselves and disable other transfers until they are
finished; deletes update the inbox and internal caches too radically to
support other parallel operations.
On systems without threads, PyMailGUI instead goes into a blocked
state during such long-running operations (it essentially stubs out the
thread-spawn operation to perform a simple function call). Because the
GUI is essentially dead without threads, covering and uncovering the GUI
during a mail load on such platforms will erase or otherwise distort its
contents. Threads are enabled by default on most platforms that run
Python (including Windows), so you probably won’t see such oddness on
your machine.
On nearly every platform, though, long-running tasks like mail
fetches and sends are spawned off as parallel threads, so that the GUI
remains active during the transfer—it continues updating itself and
responding to new user requests, while transfers occur in the
background. While that’s true of threading in most GUIs, here are two
notes regarding PyMailGUI’s specific implementation and threading
model:
As we learned earlier in this book, only the main thread
that creates windows should generally update them. See
Chapter 9
for more on this;
tkinter doesn’t support parallel GUI changes. As a result,
PyMailGUI takes care to not do anything related to the user
interface within threads that load, send, or delete email.
Instead, the main GUI thread continues responding to user
interface events and updates, and uses a timer-based event to
watch a queue for exit callbacks to be added by worker threads,
using the thread tools we implemented earlier in
Chapter 10
(
Example 10-20
). Upon
receipt, the main GUI thread pulls the callback off the queue
and invokes it to modify the GUI in the main thread.
Such queued exit callbacks can display a fetched email
message, update the mail index list, change a progress
indicator, report an error, or close an email composition
window—all are scheduled by worker threads on the queue but
performed in the main GUI thread. This scheme makes the callback
update actions automatically thread safe: since they are run by
one thread only, such GUI updates cannot overlap in time.
To make this easy, PyMailGUI stores
bound method
objects on the
thread queue, which combine both the function to be called and
the GUI object itself. Since threads all run in the same process
and memory space, the GUI object queued gives access to all GUI
state needed for exit updates, including displayed widget
objects. PyMailGUI also runs bound methods as thread actions to
allow threads to update state in general, too, subject to the
next paragraph’s rules.
Although the queued GUI update callback scheme just
described effectively restricts GUI updates to the single main
thread, it’s not enough to guarantee thread safety in general.
Because some spawned threads update shared object state used by
other threads (e.g., mail caches), PyMailGUI also uses thread
locks to prevent operations from overlapping in time if they
could lead to state collisions. This includes both operations
that update shared objects in memory (e.g., loading mail headers
and content into caches), as well as operations that may update
POP message numbers of loaded email (e.g., deletions).
Where thread overlap might be an issue, the GUI tests the
state of thread locks, and pops up a message when an operation
is not currently allowed. See the source code and this program’s
help text for specific cases where this rule is applied.
Operations
such as individual
sends and views that are largely independent can overlap
broadly, but deletions and mail header fetches cannot.
In addition, some potentially long-running save-mail
operations are threaded to avoid blocking the GUI, and this
edition uses a set object to prevent fetch threads for requests
that include a message whose fetch is in progress in order to
avoid redundant work (see the 3.0 changes review
earlier).
For more on why such things matter in general, be sure to see
the discussion of threads in GUIs in Chapters
5
,
9
, and
10
.
PyMailGUI is really just a concrete realization of concepts we’ve
explored
earlier.
Let’s return to
loading our email: because the load operation is really a
socket operation, PyMailGUI automatically connects to your email server
using whatever connectivity exists on the machine on which it is run.
For instance, if you connect to the Net over a modem and you’re not
already connected, Windows automatically pops up the standard connection
dialog. On the broadband connections that most of us use today, the
interface to your email server is normally automatic.
After PyMailGUI finishes loading your email, it populates the main
window’s scrolled listbox with all of the messages on your email server
and automatically scrolls to the most recently received message.
Figure 14-10
shows what the main window
looks like after selecting a message with a click and resizing—the text
area in the middle grows and shrinks with the window, revealing more
header columns as it grows.
Figure 14-10. PyMailGUI main window resized
Technically, the Load button fetches all your mail’s header text
the first time it is pressed, but it fetches only newly arrived email
headers on later presses. PyMailGUI keeps track of the last email
loaded, and requests only higher email numbers on later loads. Already
loaded mail is kept in memory, in a Python list, to avoid the cost of
downloading it again. PyMailGUI does not delete email from your server
when it is loaded; if you really want to not see an email on a later
load, you must explicitly
delete
it
.
Entries in the main list show just enough to give the user an idea
of what the message contains—each entry gives the concatenation of
portions of the message’s Subject, From, Date, To, and other header
lines, separated by|
characters and
prefixed with the message’s POP number (e.g., there are 13 emails in
this list). Columns are aligned by determining the maximum size needed
for any entry, up to a fixed maximum, and the set of headers displayed
can be configured in themailconfig
module. Use the horizontal scroll or expand the window to see additional
header details such as message size and mailer.
As we’ve seen, a lot of magic happens when downloading email—the
client (the machine on which PyMailGUI runs) must connect to the server
(your email account machine) over a socket and transfer bytes over
arbitrary Internet links. If things go wrong, PyMailGUI pops up standard
error dialog boxes to let you know what happened. For example, if you
type an incorrect username or password for your account (in themailconfig
module or in the password pop up),
you’ll receive the message in
Figure 14-11
. The details displayed
here are just the Python exception type and exception data. Additional
details, including a stack trace, show up in standard output (the
console window) on errors.
Figure 14-11. PyMailGUI invalid password error box