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()



[Home page] Books Code Blog Python Author Train Find ©M.Lutz