File: pymailgui-products/unzipped/PyMailGui-PP4E/PyMailGui.py
""" ################################################################################## PyMailGui 4.1 - A Python/tkinter email client. A client-side tkinter-based GUI interface for sending and receiving email. This is the program's main logic, spawned from the GUI launcher as of 4.0. See UserGuide.html for all the latest user documentation, project history, and features. See the original help string in PyMailGuiHelp.py for older usage details and enhancements. VERSIONS: Version 1.0 (Mar '01, PP2E) was a simple text-message-only email client GUI. Version 2.0 was a major, complete rewrite. The changes in 2.0 (July '05) and 2.1 (Jan '06, PP3E) were quick-access part buttons on View windows, threaded loads and deletes of local save-mail files, and checks for and recovery from message numbers out-of-synch with mail server inbox on deletes, index loads, and message loads. Version 3.0 (Dec '10, 4E) is a port to Python 3.X; uses grids instead of packed column frames for better form layout of headers in view windows; runs update() after inserting into a new text editor for accurate line positioning (see PyEdit loadFirst changes in Chapter 11); provides an HTML-based version of its help text; extracts plain-text from HTML main/only parts for display and quoting; supports separators in toolbars; addresses both message content and header Unicode encodings for fetched, sent, and saved mails (see Ch13 and Ch14); and much more (see Ch14 for the full rundown on 3.0 upgrades); fetched message decoding happens deep in the mailtools package, on mail cache load operations here; mailtools also fixes a few email package bugs (see Ch13); Version 3.1+ ('15) is the standalone release of the book's version 3.0. Version 4.0 ('17) is a major upgrade to the standalone release, with a Mac OS X port, frozen apps and executables, a GUI launches and multiple-account folder, and too many features to list here (search for "[4.0]" in the code, and see the new UserGuide's "Recent Updates" section). Version 4.1 ('22) adds a few headers per standards, and throttles down thread checks. This release is limited to the source-code package and macos app. OVERVIEW (FROM VERSION 3.0): This file implements the top-level windows and interface. PyMailGui uses a number of modules that know nothing about this GUI, but perform related tasks, some of which are developed in other sections of the book. The mailconfig module is expanded for this program. ==Modules defined elsewhere and reused here:== mailtools (package) client-side scripting chapter server sends and receives, parsing, construction (Example 13-21+) threadtools.py GUI tools chapter thread queue manangement for GUI callbacks (Example 10-20) windows.py GUI tools chapter border configuration for top-level windows (Example 10-16) textEditor.py GUI programs chapter text widget used in mail view windows, some pop ups (Example 11-4) ==Generally useful modules defined here:== popuputil.py help and busy windows, for general use messagecache.py a cache that keeps track of mail already loaded wraplines.py utility for wrapping long lines of messages html2text.py rudimentary HTML parser for extracting plain text mailconfig.py user configuration parameters: server names, fonts, etc. ==Program-specific modules defined here:== SharedNames.py objects shared between window classes and main file ViewWindows.py implementation of view, write, reply, forward windows ListWindows.py implementation of mail-server and local-file list windows PyMailGuiHelp.py (see also PyMailGuiHelp.html) user-visible help text, opened by main window bar PyMailGui.py main, top-level file (run this), with main window types ################################################################################## """ import sys, os, time # [4.0] for frozen app/exes, fix module+resource visibility (sys.path) import fixfrozenpaths # [4.0] data+scripts not in os.getcwd() if run from a cmdline elsewhere, # and __file__ may not work if running as a frozen PyInstaller executable; # use __file__ of this file for Mac apps, not module: it's in a zipfile; installpath = fixfrozenpaths.fetchMyInstallDir(__file__) # absolute # [4.0] access all data relative to '.': no relative cmdline args supported os.chdir(installpath) import mailconfig from SharedNames import appname, windows, tempPartDir from ListWindows import PyMailServer, PyMailFile # [4.0] customize the Mac's automatic top-of-display menu from PP4E.Gui.Tools.guimaker import fixAppleMenuBar ############################################################################### # top-level window classes # # View, Write, Reply, Forward, Help, BusyBox all inherit from PopupWindow # directly: only usage; askpassword calls PopupWindow and attaches; the # order matters here!--PyMail classes redef some method defaults in the # Window classes, like destroy and okayToExit: must be leftmost; to use # PyMailFileWindow standalone, imitate logic in PyMailCommon.onOpenMailFile; # [4.0] these now hide during build and unhide when built to reduce flash; ############################################################################### # and uses icon file in cwd or default in tools dir srvrname = mailconfig.popservername or 'Server' username = mailconfig.popusername or 'User' # [4.0] add user to title srvrname = username + ' on ' + srvrname # caveat: smtp srvr may vary class PyMailServerWindow(PyMailServer, windows.MainWindow): "a Tk, with extra protocol and mixed-in methods" def __init__(self): windows.MainWindow.__init__(self, appname, srvrname, withdraw=True) PyMailServer.__init__(self, withdrawn=True) class PyMailFileWindow(PyMailFile, windows.PopupWindow): "a Toplevel, with extra protocol and mixed-in methods" def __init__(self, filename): windows.PopupWindow.__init__(self, appname, filename, withdraw=True) PyMailFile.__init__(self, filename, withdrawn=True) class PyMailServerPopup(PyMailServer, windows.PopupWindow): "a Toplevel, with extra protocol and mixed-in methods (*unused*)" def __init__(self): windows.PopupWindow.__init__(self, appname, srvrname, withdraw=True) PyMailServer.__init__(self, withdrawn=True) class PyMailServerComponent(PyMailServer, windows.ComponentWindow): "a Frame, with extra protocol and mixed-in methods (*unused*)" def __init__(self): windows.ComponentWindow.__init__(self) PyMailServer.__init__(self, withdrawn=False) ############################################################################### # Prune self-cleaning temporary parts save folder on each startup ############################################################################### def pruneTempPartsFolder(): """ [4.0] On startup, prune the opened email-part files in the TempParts folder that are older than a configurable number of days. This folder never grew larger than a few dozen M in the developer's practice, but is a potential space drain. Disable by using None for the days config if you'd rather keep the temporary parts. The default is 30 days which should suffice; these files were meant to be temporaries. This code adopted (stolen) from PyEdit's auto-save pruner. Oddly, the ascii() in the filename print is crucial: without it, on Mac OS X a filename containing an emoji causes print() to fail and end pruning when not launched from a Terminal. """ helpfile = 'README-TempParts.txt' # spared reaping retaindays = mailconfig.daysToRetainTempParts # config setting if not retaindays: return # disabled: punt else: try: if os.path.exists(tempPartDir): for filename in os.listdir(tempPartDir): if filename == helpfile: continue pathname = os.path.join(tempPartDir, filename) modtime = os.path.getmtime(pathname) # epoch seconds nowtime = time.time() # ditto dayssecs = retaindays * 24 * 60 * 60 if nowtime > modtime + dayssecs: print('TempParts pruning:', ascii(pathname)) try: os.remove(pathname) except Exception as why: print('Prune skipping failed file:', ascii(why)) except Exception as why: print('TempParts pruner failed:', why) # but continue PyMailGUI else: print('TempParts pruner finished normally') ############################################################################### # When run as a top-level program: create main mail-server list window ############################################################################### if __name__ == '__main__': # [4.0] self-clean temp folder as configured; # this does not require a widget "self" to run; # run before GUI build to avoid pauses/flashes; pruneTempPartsFolder() # open server window: a Tk() that remains for the entire # program run, and is the implicit parent of all popups rootwin = PyMailServerWindow() # [4.0] customize Mac default menus for this program, set app help/quit; # ViewWindows and ListWindows make calls for View/Filelist child windows; fixAppleMenuBar(rootwin, appname='PyMailGUI', helpaction=rootwin.onShowHelp, # all menus use these aboutaction=None, # quit ends entire app quitaction=rootwin.quit) # [4.0] Add support for multiprocessing in embedded PyEdit's Grep; # this must be run in __main__, so that means here for PyMailGUI; # used for single-file PyInstaller frozen binaries on Windows only: # a no-op in all other contexts (Mac app, Linux exe, source code); # PyEdit has already imported and run multiprocessing_exe_patch.py; import multiprocessing multiprocessing.freeze_support() # open save-mail file windows if args (demo, mostly); # save files are loaded in threads ([4.0] ignore Apple OpenDoc event) if len(sys.argv) > 1: # 3.0: fix to add len() for savename in sys.argv[1:]: rootwin.onOpenMailFile(savename) rootwin.lift() if sys.platform.startswith('darwin'): #------------------------------------------------------- # [4.0] on Mac OS X ensure initial Aqua active style # for the main window's checkbutton, else simple grey; # due to a Tk 8.5 bug, can still lose active style on # minimize/restore and common dialogs - click as needed; #------------------------------------------------------- rootwin.lift() #------------------------------------------------------- # [4.0] for main window, fix tk focus loss on deiconify # by catching Dock clicks and running the heinous hack # copied from mergeall below; this works for the main # server window only; focus_force fixes common dialogs; # # caveat: this doesn't reopen the launcher if closed, # and may open an arbitrary account window if many open; # # UPDATE: this is fairly subtle; clicking the Dock item # for an account window lifts that window (as it should) # but clicking the app always raises the _last_ window # opened by the app, not the launcher; there seems no # way to fix this, as the only context in the reopen # handler is the window it's registered for, and this # seems to be dropped by tkinter/Tk (the app handler is # app-global, and is always the last one registered); # # TO IMPROVE (not fix), the launcher's close button is # now really iconify(), so it remains in the dock and # can be reopened by a click there (unless forcibly Quit # in the dock); the alternative is to create/delete a # '.launcheropen' file on start/exit, and checked here: # if present, open this account's window, else run the # launcher again to allow new account opens (hack!); #------------------------------------------------------- from tkinter import Toplevel def onReopen(): #print(root.state()) # always normal rootwin.lift() rootwin.update() temp = Toplevel() temp.lower() temp.destroy() rootwin.createcommand('::tk::mac::ReopenApplication', onReopen) rootwin.mainloop()