PyMailCGI supports
two main functions, as links on the root page: composing and
sending new mail to others, and viewing incoming mail. The View function
leads to pages that let users read, reply to, forward, and delete existing
email. Since the Send function is the simplest, let’s start with its pages
and scripts first.
The root
page Send function steps users through two other pages:
one to edit a message and one to confirm delivery. When you click on the
Send link on the main page in
Figure 16-2
,
the Python CGI script in
Example 16-3
runs on the web
server.
Example 16-3. PP4E\Internet\Web\PyMailCgi\cgi-bin\onRootSendLink.py
#!/usr/bin/python
"""
################################################################################
On 'send' click in main root window: display composition page
################################################################################
"""
import commonhtml
from externs import mailconfig
commonhtml.editpage(kind='Write', headers={'From': mailconfig.myaddress})
No, this file wasn’t truncated; there’s not much to see in this
script because all the action has been encapsulated in thecommonhtml
andexterns
modules. All that we can tell here is
that the script calls something namededitpage
to generate a reply, passing in
something calledmyaddress
for its
“From” header.
That’s by design—by hiding details in shared utility modules we
make top-level scripts such as this much easier to read and write, avoid
code redundancy, and achieve a common look-and-feel to all our pages.
There are no inputs to this script either; when run, it produces a page
for composing a new message, as shown in
Figure 16-3
.
Figure 16-3. PyMailCGI send (write) page
Most of the composition page is self-explanatory—fill in headers
and the main text of the message (a “From” header and standard signature
line are initialized from settings in themailconfig
module, discussed further ahead).
The Choose File buttons open file selector dialogs, for picking an
attachment. This page’s interface looks very different from the
PyMailGUI client program in
Chapter 14
, but
it is functionally very similar. Also notice the top and bottom of this
page—for reasons explained in the next section, they are going to look
the same in all the pages of our
system.
As usual,
the HTML of the edit page in
Figure 16-3
names its handler
script. When we click its Send button,
Example 16-4
runs on the server to
process our inputs and send the mail message.
Example 16-4. PP4E\Internet\Web\PyMailCgi\cgi-bin\onEditPageSend.py
#!/usr/bin/python
"""
################################################################################
On submit in edit window: finish a write, reply, or forward;
in 2.0+, we reuse the send tools in mailtools to construct and send the message,
instead of older manual string scheme; we also inherit attachment structure
composition and MIME encoding for sent mails from that module;
3.0: CGI uploads fail in the py3.1 cgi module for binary and incompatibly-encoded
text, so we simply use the platform default here (cgi's parser does no better);
3.0: use simple Unicode encoding rules for main text and attachments too;
################################################################################
"""
import cgi, sys, commonhtml, os
from externs import mailtools
savedir = 'partsupload'
if not os.path.exists(savedir):
os.mkdir(savedir)
def saveAttachments(form, maxattach=3, savedir=savedir):
"""
save uploaded attachment files in local files on server from
which mailtools will add to mail; the 3.1 FieldStorage parser
and other parts of cgi module can fail for many upload types,
so we don't try very hard to handle Unicode encodings here;
"""
partnames = []
for i in range(1, maxattach+1):
fieldname = 'attach%d' % i
if fieldname in form and form[fieldname].filename:
fileinfo = form[fieldname] # sent and filled?
filedata = fileinfo.value # read into string
filename = fileinfo.filename # client's pathname
if '\\' in filename:
basename = filename.split('\\')[-1] # try DOS clients
elif '/' in filename:
basename = filename.split('/')[-1] # try Unix clients
else:
basename = filename # assume dir stripped
pathname = os.path.join(savedir, basename)
if isinstance(filedata, str): # 3.0: rb needs bytes
filedata = filedata.encode() # 3.0: use encoding?
savefile = open(pathname, 'wb')
savefile.write(filedata) # or a with statement
savefile.close() # but EIBTI still
os.chmod(pathname, 0o666) # need for some srvrs
partnames.append(pathname) # list of local paths
return partnames # gets type from name
#commonhtml.dumpstatepage(0)
form = cgi.FieldStorage() # parse form input data
attaches = saveAttachments(form) # cgi.print_form(form) to see
# server name from module or get-style URL
smtpservername = commonhtml.getstandardsmtpfields(form)
# parms assumed to be in form or URL here
from commonhtml import getfield # fetch value attributes
From = getfield(form, 'From') # empty fields may not be sent
To = getfield(form, 'To')
Cc = getfield(form, 'Cc')
Subj = getfield(form, 'Subject')
text = getfield(form, 'text')
if Cc == '?': Cc = ''
# 3.0: headers encoded per utf8 within mailtools if non-ascii
parser = mailtools.MailParser()
Tos = parser.splitAddresses(To) # multiple recip lists: ',' sept
Ccs = (Cc and parser.splitAddresses(Cc)) or ''
extraHdrs = [('Cc', Ccs), ('X-Mailer', 'PyMailCGI 3.0')]
# 3.0: resolve main text and text attachment encodings; default=ascii in mailtools
bodyencoding = 'ascii'
try:
text.encode(bodyencoding) # try ascii first (or latin-1?)
except (UnicodeError, LookupError): # else use tuf8 as fallback (or config?)
bodyencoding = 'utf-8' # tbd: this is more limited than PyMailGUI
# 3.0: use utf8 for all attachments; we can't ask here
attachencodings = ['utf-8'] * len(attaches) # ignored for non-text parts
# encode and send
sender = mailtools.SilentMailSender(smtpservername)
try:
sender.sendMessage(From, Tos, Subj, extraHdrs, text, attaches,
bodytextEncoding=bodyencoding,
attachesEncodings=attachencodings)
except:
commonhtml.errorpage('Send mail error')
else:
commonhtml.confirmationpage('Send mail')
This script gets mail header and text input information from the
edit page’s form (or from query parameters in an explicit URL) and sends
the message off using Python’s standardsmtplib
module, courtesy of themailtools
package. We studiedmailtools
in
Chapter 13
, so I won’t say much more about it
now. Note, though, that because we are reusing its send call, sent mail
is automatically saved in a
sentmail.txt
file on the server; there are no
tools for viewing this in PyMailCGI itself, but it serves as a
log.
New in version 2.0, thesaveAttachments
function grabs any part files
sent from the browser and stores them in temporary local files on the
server from which they will be added to the mail when sent. We covered
CGI upload in detail at the end of
Chapter 15
; see that discussion for more on how
the code here works (as well as its limitations in Python 3.1 and this
edition—we’re attaching simple text here to accommodate). The business
of attaching the files to the mail itself is automatic inmailtools
.
A utility incommonhtml
ultimately fetches the name of the SMTP server to receive the message
from either themailconfig
module or
the script’s inputs (in a form field or URL query parameter). If all
goes well, we’re presented with a generated confirmation page, as
captured in
Figure 16-4
.
Figure 16-4. PyMailCGI send confirmation page
Open file
sentmail.txt
in
PyMailCGI’s source directory if you want to see what the resulting
mail’s raw text looks like when sent (or fetch the message in an email
client with a raw text view, such as PyMailGUI). In this version, each
attachment part is MIME encoded per Base64 with UTF-8 Unicode
encoding in the multipart message, but the main text part
is sent as simple ASCII if it works as such.
As we’ll see, this send mail script is also used to deliver
reply
and
forward
messages for
incoming POP mail. The user interface for those operations is slightly
different for composing new email from scratch, but as in PyMailGUI, the
submission handler logic has been factored into the same, shared
code—replies and forwards are really just mail send operations with
quoted text and preset header fields.
Notice that there are no usernames or passwords to be found here;
as we saw in
Chapter 13
, SMTP usually
requires only a server that listens on the SMTP port, not a user account
or password. As we also saw in that chapter, SMTP send operations that
fail either raise a Python exception (e.g., if the server host can’t be
reached) or return a dictionary of failed recipients; ourmailtools
package modules insulate us from
these details by always raising an exception in
either case.
If there is a
problem during mail delivery, we get an error page such as
the one shown in
Figure 16-5
. This page
reflects a failed recipient and includes a stack trace generated by the
standard
library’straceback
module. On errors Python detects, the Python error message and extra
details would be displayed.
Figure 16-5. PyMailCGI send error page
It’s also worth pointing out that thecommonhtml
module encapsulates the generation
of both the confirmation and the error pages so that all such pages look
the same in PyMailCGI no matter where and when they are produced. Logic
that generates the mail edit page incommonhtml
is reused by the reply and forward
actions, too (but with different mail headers).
In fact,commonhtml
makes
all pages look similar—it also provides common page
header
(top) and
footer
(bottom) generation functions, which are used everywhere in the system.
You may have already noticed that all the pages so far follow the same
pattern: they start with a title and horizontal rule, have something
unique in the middle, and end with another rule, followed by a Python
icon and link at the bottom. This
common
look-and-feel
is the product of shared code incommonhtml
; it generates everything but the
middle section for every page in the system (except the root page, a
static HTML file).
Most important, if we ever change the header and footer format
functions in thecommon
html
module, all our page’s headers and
footers will automatically be updated. If you are interested in seeing
how this encapsulated logic works right now, flip ahead to
Example 16-14
. We’ll explore its code
after we study the rest of the mail site’s pages.