File: class/Extras/Code/pp3e/mailtools.py

File: class/Extras/Code/pp3e/mailtools.py

########################################
# interface to mail server transfers
# designed to be mixed-in to subclasses
# which use the methods defined here;
# class allows askPopPassword to differ
# loads all if server doesn't do top
# doesn't handle threads or UI here
# progress callback funcs get status
# all calls raise exceptions on error
########################################
#???? saveparts: open in text mode for text/ contypes?
import mailconfig
import poplib, smtplib, os, mimetypes
import email.Parser, email.Utils, email.Encoders
from email.Message import Message
from email.MIMEMultipart import MIMEMultipart
from email.MIMEAudio import MIMEAudio
from email.MIMEImage import MIMEImage
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
class MailTool: # super class for all mail tools
 def trace(self, message): # redef me to disable or log to file
 print message
####################################
# parsing and attachment extraction
####################################
class MailParser(MailTool):
 """
 methods for parsing message text, attachements
 subtle thing: Message object payloads are either a simple
 string for non-multipart messages, or a list of Message
 objects if multipart (possiby nested); we don't need to
 distinguish between the two cases here, because the Message
 walk generator always returns self first, and so works fine
 on non-multipart messages too (a single object is walked);
 for simple messages, the message body is always considered
 here to be the sole part of the mail; for miltipart messages,
 the parts list includes the main message text, as well as all
 attachments; this allows simple messages not of type text to
 be handled like attachments in a UI (e.g., saved, opened).
 """
 
 def saveParts(self, savedir, message):
 """
 store all parts of a message as files in a local directory;
 returns [('maintype/subtype', 'filename')] list for use by
 callers, but does not open any parts or attachments here;
 """
 if not os.path.exists(savedir):
 os.mkdir(savedir)
 partfiles = []
 for (ix, part) in enumerate(message.walk()): # walk includes message
 maintype = part.get_content_maintype() # ix includes multiparts 
 if maintype == 'multipart':
 continue # multipart/*: container
 else: # else save part to file
 filename, contype = self.partName(part, ix)
 fullname = os.path.join(savedir, filename)
 fileobj = open(fullname, 'wb') # use binary mode
 fileobj.write(part.get_payload(decode=1)) # decode base64,etc
 fileobj.close()
 partfiles.append((contype, fullname)) # for caller to open
 return partfiles
 
 def partsList(self, message):
 """"
 return a list of filenames for all parts of an
 already-parsed message, using same file name logic
 as saveParts, but do not store the part files here
 """
 partfiles = []
 for (ix, part) in enumerate(message.walk()):
 maintype = part.get_content_maintype()
 if maintype == 'multipart':
 continue
 filename, contype = self.partName(part, ix)
 partfiles.append(filename)
 return partfiles
 def partName(self, part, ix):
 """
 extract filename and content type from message part;
 filename: tries Content-Disposition, then Content-Type
 name param, or generates one based on mimetype guess;
 """
 filename = part.get_filename() # filename in msg hdrs?
 contype = part.get_content_type() # lower maintype/subtype
 if not filename:
 filename = part.get_param('name') # try content-type name
 if not filename:
 if contype == 'text/plain': # hardcode plain text ext
 ext = '.txt' # else guesses .ksh!
 else:
 ext = mimetypes.guess_extension(contype)
 if not ext: ext = '.bin' # use a generic default
 filename = 'part-%03d%s' % (ix, ext)
 return (filename, contype)
 def findMainText(self, message):
 """
 for text-oriented clients, return the first text part;
 for the payload of a simple message, or all parts of
 a multipart message, looks for text/plain, then text/html,
 then text/*, before deducing that there is no text to
 display; this is a heuristic, but covers most simple,
 multipart/alternative, and multipart/mixed messages;
 content-type defaults to text/plain if none in simple msg; 
 handles message nesting at top level by walking instead
 of list scans; if non-multipart but type is text/html,
 returns the htlm as the text with an html type: caller
 may open in webbrowser; if non-multipart and not text,
 no text to display: save/open in UI; caveat: does not
 try to concatenate multiple inline text/plain parts
 """
 # try to find a plain text
 for part in message.walk(): # walk visits messge
 type = part.get_content_type() # if non-multipart
 if type == 'text/plain':
 return type, part.get_payload(decode=1) # may be base64
 # try to find a html part
 for part in message.walk():
 type = part.get_content_type()
 if type == 'text/html':
 return type, part.get_payload(decode=1) # caller renders
 # try any other text type, including xml
 for part in message.walk():
 if part.get_content_maintype() == 'text':
 return part.get_content_type(), part.get_payload(decode=1)
 # punt: could use first part, but it's not marked as text 
 return 'text/plain', '[No text to display]'
 # returned when parses fail
 errorMessage = Message()
 errorMessage.set_payload('[Unable to parse message - format error]')
 
 def parseHeaders(self, mailtext):
 """
 parse headers only, return root email.Message object
 stops after headers parsed, even if nothing else follows (top)
 email.Message object is a mapping for mail header fields
 payload of message object is None, not raw body text
 """
 try:
 return email.Parser.Parser().parsestr(mailtext, headersonly=True)
 except:
 return self.errorMessage
 def parseMessage(self, fulltext):
 """
 parse entire message, return root email.Message object
 payload of message object is a string if not is_multipart()
 payload of message object is more Messages if multiple parts
 the call here same as calling email.message_from_string()
 """
 try:
 return email.Parser.Parser().parsestr(fulltext) # may fail!
 except:
 return self.errorMessage # or let call handle? can check return
 def parseMessageRaw(self, fulltext):
 """
 parse headers only, return root email.Message object
 stops after headers parsed, for efficiency (not yet used here)
 payload of message object is raw text of mail after headers
 """
 try:
 return email.Parser.HeaderParser().parsestr(fulltext)
 except:
 return self.errorMessage
####################################
# send messages, add attachments
####################################
class MailSender(MailTool):
 """
 send mail: format message, interface with SMTP server
 works on any machine with Python+Inet, doesn't use cmdline mail
 """
 def __init__(self, smtpserver=None):
 self.smtpServerName = smtpserver or mailconfig.smtpservername
 def sendMessage(self, From, To, Subj, extrahdrs, bodytext, attaches):
 """
 format,send mail: blocks caller, thread me in a gui
 bodytext is main text part, attaches is list of filenames
 extrahdrs is list of (name, value) tuples to be added
 raises uncaught exception if send fails for any reason
 
 assumes that To, Cc, Bcc hdr values are lists of 1 or more already
 stripped addresses (possibly in full name+<addr> format); client
 must split these on delimiters, parse, or use multi-line input;
 note that smtp allows full name+<addr> format in recipients
 """
 if not attaches:
 msg = Message()
 msg.set_payload(bodytext)
 else:
 msg = MIMEMultipart()
 self.addAttachments(msg, bodytext, attaches)
 recip = To
 msg['From'] = From
 msg['To'] = ', '.join(To) # poss many: addr list
 msg['Subject'] = Subj # servers reject ';' sept
 msg['Date'] = email.Utils.formatdate() # curr datetime, rfc2822 utc
 for name, value in extrahdrs: # Cc, Bcc, X-Mailer, etc.
 if value:
 if name.lower() not in ['cc', 'bcc']:
 msg[name] = value
 else:
 msg[name] = ', '.join(value) # add commas between
 recip += value # some servers reject ['']
 fullText = msg.as_string() # generate formatted msg
 # sendmail call raises except if all Tos failed,
 # or returns failed Tos dict for any that failed
 
 self.trace('Sending to...'+ str(recip))
 self.trace(fullText[:256])
 server = smtplib.SMTP(self.smtpServerName) # this may fail too
 try:
 failed = server.sendmail(From, recip, fullText) # except or dict
 finally:
 server.quit() # iff connect okay
 if failed:
 class SomeAddrsFailed(Exception): pass
 raise SomeAddrsFailed('Failed addrs:%s\n' % failed)
 self.trace('Send exit')
 def addAttachments(self, mainmsg, bodytext, attaches):
 # format a multi-part message with attachments
 msg = MIMEText(bodytext) # add main text/plain part
 mainmsg.attach(msg)
 for filename in attaches: # absolute or relative paths
 if not os.path.isfile(filename): # skip dirs, etc.
 continue
 
 # guess content type from file extension, ignore encoding
 contype, encoding = mimetypes.guess_type(filename)
 if contype is None or encoding is not None: # no guess, compressed?
 contype = 'application/octet-stream' # use generic default
 self.trace('Adding ' + contype)
 # build sub-Message of apropriate kind
 maintype, subtype = contype.split('/', 1)
 if maintype == 'text':
 data = open(filename, 'r')
 msg = MIMEText(data.read(), _subtype=subtype)
 data.close()
 elif maintype == 'image':
 data = open(filename, 'rb')
 msg = MIMEImage(data.read(), _subtype=subtype)
 data.close()
 elif maintype == 'audio':
 data = open(filename, 'rb')
 msg = MIMEAudio(data.read(), _subtype=subtype)
 data.close()
 else:
 data = open(filename, 'rb')
 msg = MIMEBase(maintype, subtype)
 msg.set_payload(data.read())
 data.close() # make generic type
 email.Encoders.encode_base64(msg) # encode using Base64
 # set filename and attach to container
 basename = os.path.basename(filename)
 msg.add_header('Content-Disposition',
 'attachment', filename=basename)
 mainmsg.attach(msg)
 # text outside mime structure, seen by non-MIME mail readers
 mainmsg.preamble = 'A multi-part MIME format message.\n'
 mainmsg.epilogue = '' # make sure message ends with a newline
####################################
# retrieve mail from a pop server
####################################
class MailFetcher(MailTool):
 """
 fetch mail: connect, fetch headers+mails, delete mails
 works on any machine with Python+Inet; subclass me to cache
 implemented with the POP protocol; IMAP requires new class
 """
 def __init__(self, popserver=None, popuser=None, hastop=True):
 self.popServer = popserver or mailconfig.popservername
 self.popUser = popuser or mailconfig.popusername
 self.srvrHasTop = hastop
 self.popPassword = None
 
 def connect(self):
 self.trace('Connecting...')
 self.getPassword() # file, gui, or console
 server = poplib.POP3(self.popServer) 
 server.user(self.popUser) # connect,login pop server
 server.pass_(self.popPassword) # pass is a reserved word
 self.trace(server.getwelcome()) # print returned greeting 
 return server
 def downloadMessage(self, msgnum):
 # load full text of one msg
 self.trace('load '+str(msgnum))
 server = self.connect()
 try:
 resp, msglines, respsz = server.retr(msgnum)
 finally:
 server.quit()
 return '\n'.join(msglines) # concat lines for parsing
 def downloadAllHeaders(self, progress=None, loadfrom=1):
 """
 get sizes, headers only, for all or new msgs
 begins loading from message number loadfrom
 use loadfrom to load newly-arrived mails only
 use downloadMessage to get the full msg text
 """
 if not self.srvrHasTop: # not all servers support TOP
 return self.downloadAllMsgs(progress) # naively load full msg text
 else:
 self.trace('loading headers')
 server = self.connect() # mbox now locked until quit
 try:
 resp, msginfos, respsz = server.list() # 'num size' lines list
 msgCount = len(msginfos) # alt to srvr.stat[0]
 msginfos = msginfos[loadfrom-1:] # drop already-loadeds
 allsizes = [int(x.split()[1]) for x in msginfos]
 allhdrs = []
 for msgnum in range(loadfrom, msgCount+1): # poss empty
 if progress: progress(msgnum, msgCount) # callback?
 resp, hdrlines, respsz = server.top(msgnum, 0) # hdrs only
 allhdrs.append('\n'.join(hdrlines))
 finally:
 server.quit() # make sure unlock mbox
 assert len(allhdrs) == len(allsizes)
 self.trace('load headers exit')
 return allhdrs, allsizes, False
 def downloadAllMessages(self, progress=None, loadfrom=1):
 # load full message text for all msgs, despite any caching
 self.trace('loading full messages')
 server = self.connect()
 try:
 (msgCount, msgBytes) = server.stat()
 allmsgs = []
 allsizes = []
 for i in range(loadfrom, msgCount+1): # empty if low >= high
 if progress: progress(i, msgCount)
 (resp, message, respsz) = server.retr(i) # save text on list
 allmsgs.append('\n'.join(message)) # leave mail on server
 allsizes.append(respsz) # diff from len(msg)
 finally:
 server.quit() # unlock the mail box
 assert len(allmsgs) == (msgCount - loadfrom) + 1 # msg nums start at 1
 assert sum(allsizes) == msgBytes
 return allmsgs, allsizes, True
 def deleteMessages(self, msgnums, progress=None):
 # delete multiple msgs off server
 server = self.connect()
 try:
 for (ix, msgnum) in enumerate(msgnums): # dont reconnect for each
 if progress: progress(ix+1, len(msgnums))
 server.dele(msgnum) 
 finally: # changes msgnums: reload
 server.quit()
 
 def getPassword(self):
 """
 get pop password if not yet known
 not required until go to server
 from client-side file or subclass method
 """
 if not self.popPassword:
 try:
 localfile = open(mailconfig.poppasswdfile)
 self.popPassword = localfile.readline()[:-1]
 self.trace('local file password' + repr(self.popPassword))
 except:
 self.popPassword = self.askPopPassword()
 def askPopPassword(self):
 assert False, 'Subclass must define method'
class MailFetcherConsole(MailFetcher):
 def askPopPassword(self):
 import getpass
 prompt = 'Password for %s on %s?' % (self.popUser, self.popServer)
 return getpass.getpass(prompt)
##################################
# self-test when run as a program
##################################
if __name__ == '__main__':
 sender = MailSender()
 sender.sendMessage(From = mailconfig.myaddress,
 To = [mailconfig.myaddress],
 Subj = 'testing 123',
 extrahdrs = [('X-Mailer', 'mailtools.py')],
 bodytext = 'Here is my source code',
 attaches = ['mailtools.py'])
 fetcher = MailFetcherConsole()
 def status(*args): print args
 
 hdrs, sizes, all = fetcher.downloadAllHeaders(status)
 for num, hdr in enumerate(hdrs[:5]):
 print hdr
 if raw_input('load mail?') in ['y', 'Y']:
 print fetcher.downloadMessage(num+1), '\n', '-'*70
 
 msgs, sizes, all = fetcher.downloadAllMessages(status)
 for msg in msgs[:5]:
 print msg, '\n', '-'*70
 raw_input('Press Enter to exit')



AltStyle によって変換されたページ (->オリジナル) /