File: pymailgui-products/unzipped/Launch_PyMailGUI.py

#!/usr/bin/python3
"""
================================================================================
Run this file to start PyMailGUI, and select email accounts to open. [4.0]

To use this program to send and receive email on your own email accounts,
create your own account configurations file(s) in its "MailConfigs" folder.
This GUI launcher will use all the "mailconfig_*.py" files in "MailConfigs"
to create its account-selection list.  Select name "<account>" in the GUI
to choose your account settings file "MailConfigs/mailconfig_<account>.py".

Account files can contain any Python code, including exec()s or imports to 
run other files.  The PyMailGUI program will run all the code in the selected
account's file, such that any assignments in the account's file will replace
and customize default settings in the base file, located at:

     PyMailGui-PP4E/mailconfig.py

The "(default)" choice in the GUI uses the base file without extension; for
a single email account, you can edit the base file and run PyMailGUI directly.
 ----
 UPDATE: the special "(default)" choice hard-coded here was replaced with a
 "mailconfig_defaultbase.py" MailConfigs file which does _not_ extend the base
 at all.  This is functionally equivalent to the former "(default)", but allows
 users to delete the base file if unused to remove it from the launcher's GUI.
 The accounts lits is also sorted, so its order is the same on all platforms.

Technically, the spawned PyMailGUI is passed a command-line argument here that
instructs it to run the selected account's "MailConfigs" file in the scope of
PyMailGUI's default "mailconfigs.py" base-file module, with the current
directory and import paths redirected to "MailConfigs" here.  The net effect
is that the account file's assignments override the default settings in the
base file.  This standalone release's tree structure also allows PyMailGUI to
run without any PYTHONPATH setup (e.g., via icon click or desktop shortcut).

The account configuration files shipped serve as examples to emulate, and can
be used to test-drive the system and view saved mail files (see "SavedMail/"
and "sentmail.txt" in "PyMailGui-PP4E" for examples to Open).  However, you
must create your own accounts' files to process your own email live - all
of the shipped example accounts require a password to load or send email.
All shipped config files can also be deleted, to remove them from the GUI.

This version of the launcher replaces that in the book PP4E, as well as two
earlier and more complex variants whose code is available in "docetc/obsolete".
This new scheme is backward-compatible, except that the older "_mailconfig.py"
should not and cannot be imported, as it is no longer copied from the base.
Instead, account files in "MailConfigs" here extend the base file implicitly.

See "README.txt" in the "MailConfigs" folder as well as the main doc file
"UserGuide.html" here for more usage and program details.
================================================================================
"""

import os, glob, sys, webbrowser, subprocess
from tkinter import *
trace = True

# this script isn't too platform-specific, but avoid repeating this
RunningOnMac     = sys.platform.startswith('darwin')
RunningOnWindows = sys.platform.startswith('win')
RunningOnLinux   = sys.platform.startswith('linux')

# for frozen app/exes, fix module+resource visibility (sys.path)
import fixfrozenpaths

# script 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;
launcherpath = fixfrozenpaths.fetchMyInstallDir(__file__)   # absolute

# access all data relative to '.': no cmdline args to this script
os.chdir(launcherpath) 

# too late to change the '-'...
import importlib
guimaker = importlib.import_module('PyMailGui-PP4E.PP4E.Gui.Tools.guimaker')

# print to console too, if there is one
welcomemessage = """Welcome to PyMailGUI - a POP/SMTP desktop-GUI email client.
License: this program is provided freely, but with no warranties of any kind.
See this program's README.txt and UserGuide.html for usage details.
"""
print(welcomemessage, flush=True)

# to view the import path in the Mac console log
# for p in sys.path: 
#     sys.stderr.write('==> ' + repr(p) + '\n'); sys.stderr.flush()

#===============================================================================

def onOpenAccount(acctname):
    """
    ----------------------------------------------------------------------
    Spawn a shell command to launch PyMailGUI with a mailconfig arg;
    subprocess.Popen() spawns the command as an independent process,
    and maps all started PyMailGUI's stdouts to the same single console,
    which lives on if the launcher process is closed and lingers until
    the last related process exits (it's inherited by child processes by
    default).  Alternative spawners: os.system() blocks its caller (this),
    os.popen() doesn't change cwd and generates errors on exit due to
    pipes, and os.spawnv() may or may not work portably (untested here).
    CAUTION: closing the shared Windows console closes all account GUIs!
    [4.0.1] Set child process's stdout to /dev/null for Mac apps only, to 
    discard output and avoid broken pipe-exceptions at arbitrary prints.
    ----------------------------------------------------------------------
    """
    # [4.0.1] mac apps broken-pipe fix
    if hasattr(sys, 'frozen') and sys.frozen == 'macosx_app':
        outputstream = subprocess.DEVNULL
    else:
        outputstream = None   # also the default: inherit parent's 

    # command switch for acct
    acctfilepath = os.path.join(launcherpath,   # must be abs: in mailconfig ..
                                'MailConfigs',
                                'mailconfig_%s.py' % acctname)
    acctarg      = '-mailconfig=' + acctfilepath

    # where/what to spawn
    scriptdir    = 'PyMailGui-PP4E'
    if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):
        # pyinstaller exe
        freezename = 'PyMailGui.exe' if RunningOnWindows else 'PyMailGui'
        freezepath = os.path.join(launcherpath,   # must be abs: argv[0] post cd
                                  scriptdir,
                                  freezename)
        commandline  = [freezepath]
    else:
        # py2app Mac app or source (original)
        scriptfile   = 'PyMailGui.py'
        thispyexe    = sys.executable
        commandline  = [thispyexe, scriptfile]   # will cd in py process 

    # extend command sequence (args auto-quoted)
    commandline += [acctarg]                   # (default) MailConfigs file

    subprocess.Popen(
        commandline,           # spawn command line as an independent process
        cwd=scriptdir,         # cd to here before spawning, for files, etc.
        stdout=outputstream,   # [4.0.1] mac apps broken-pipe error fix
        stderr=outputstream)

    # spawned PyMailGUI's mailconfig.py will exec the account file in its scope

#===============================================================================
    
def makegui(win):
    """
    ----------------------------------------------------------------------
    Build the GUI's widgets on window 'win'.  This GUI stays up after
    spawning PyMailGUIs to allow other accounts to be opened.  Closing
    the launcher GUI does not close opened PyMailGUI account windows,
    and the launcher and all its spawn share the same single console.

    CAUTION: on Windows, closing the shared console closes all PyMailGUIs;
    spawned; this is either a feature or bug, depending on when you ask!;

    UPDATE: on Macs, the launcher window's close button now just iconifies
    the window, so it can be reopened with a Dock click; else a reopen
    event on app-click lifts the latest account (if any), and does not 
    restart a closed launcher: must close all accounts to open another!;
    the launcher can still be fully closed by an explicit right-click+Quit
    on its dock item; see also PyMailGui.py's __main__ for more details;
    ----------------------------------------------------------------------
    """
    # we already ran a cd to script dir for icons, help, images
    global gifimg  # save an image reference (still required by Tk?)

    # main window
    win.title('PyMailGUI Launcher')
    if RunningOnMac:
        window_closer = win.iconify
    else:
        window_closer = win.quit
    win.protocol('WM_DELETE_WINDOW', window_closer)    # window-border close

    # custom window or app bar icon, where supported
    try:
        if RunningOnWindows:
            # Windows, all contexts
            iconpath = os.path.join('docetc', 'ICONS', 'pymailguiplainset.ico') 
            win.iconbitmap(iconpath)
        elif RunningOnLinux:
            # Linux , Tk 8.5+, app bar
            iconpath = os.path.join('docetc', 'ICONS', 'mb_plain.gif') 
            imgobj = PhotoImage(file=iconpath)
            win.iconphoto(True, imgobj)
        elif RunningOnMac or True:
            # Mac OS X: neither works (requires an app)
            raise NotImplementedError
    except Exception as why:
        pass   # bad file or platform

    # "splash" screen at top with Help link
    topfrm = Frame(win)
    topfrm.pack(fill=X)

    # use larger font on Mac OS X
    msgsize = 14 if RunningOnMac else 11 
    Label(topfrm, text='Welcome to PyMailGUI  ',
          bg='white', font=('Arial', msgsize, 'bold italic')
          ).pack(expand=YES, fill=BOTH, side=RIGHT)

    # image + help-link: gif works on all Pythons/Tks
    imgpath = os.path.join('docetc', 'ICONS', 'mb_plain.gif') 
    gifimg = PhotoImage(file=imgpath) 
    imglab = Label(topfrm, image=gifimg, bg='white')
    imglab.pack(expand=NO, side=LEFT)
    helpfile = 'file:%s/%s' % (os.getcwd(), 'UserGuide.html')
    def helpopen():
        webbrowser.open(helpfile)
    imglab.bind('<Button-1>', lambda event: helpopen())
    imglab.config(cursor='question_arrow')  # or 'hand2'?

    # get account names from filenames: MailConfigs/mailconfig_<account>.py
    mods = glob.glob('MailConfigs' + os.sep + 'mailconfig_*.py')
    keys = [mod.split('_', 1)[1][:-3] for mod in mods]    # allow >1 '_' in acct name
    keys = sorted(keys)                                   # same order on all platforms

    # account select list
    radiofrm = Frame(win, relief=GROOVE, border=2, width=25)
    radiofrm.pack(fill=BOTH, expand=YES)
    Label(radiofrm, text='Select your email account').pack()

    #keys.append('(default)')   # now via no-op defaultbase MailConfigs file
    acctvar = StringVar()
    for key in keys:
        Radiobutton(radiofrm,
                    text=key,
                    variable=acctvar,
                    value=key).pack(anchor=NW)
    acctvar.set(keys[0])  # default to first

    # action buttons
    Button(win, text='Open Account',
                command=lambda: onOpenAccount(acctvar.get())).pack(side=LEFT)
    Button(win, text='Close Launcher',
                command=window_closer).pack(side=RIGHT)
    
    # firm up default menus on Mac OS X (only) while the launcher is open
    guimaker.fixAppleMenuBar(win, 'PyMailGUI',
               helpaction=helpopen, aboutaction=None, quitaction=win.quit)

#===============================================================================

if __name__ == '__main__':
    win = Tk()
    makegui(win)
    
    if RunningOnMac:
        #--------------------------------------------------------------------
        # [4.0] required on Mac OS X (only), else the checkbuttons in the
        # main window are not displayed in Aqua (blue) active-window style
        # until users click another window and click this program's window;
        # caveat: can still lose active style on iconify and common dialogs;
        # this is a bug in AS's Mac Tk 8.5 -- it's not present in other Tk
        # ports, and IDLE search dialogs have the same issue;  for reasons
        # TBD, it's enough to use just the lift() below for pymailgui when 
        # it is run from a command line, but the full bit here is required 
        # when run by mac pylaucher on a click;  ditto for pymailgui, but
        # mergeall requires all 3 steps in both contexts...
        #
        # UPDATE: focus is now restored after common dialog closes by a 
        # focus_force(), and on deiconifies (unhides) by catching Dock 
        # clicks and running the heinous hack copied from mergeall below;
        #
        # UPDATE: on Macs, the launcher's close simply iconifies it so it
        # can be reopened via its Dock item, else reopen always lifts a
        # latest account window only; see note in makegui() above; 
        #--------------------------------------------------------------------

        # fix tk focus loss on startup
        win.withdraw()
        win.lift()
        win.after_idle(win.deiconify)

        # fix tk focus loss on deiconify
        def onReopen():
            #print(root.state())    # always normal
            win.lift()
            win.update()
            temp = Toplevel()
            temp.lower()
            temp.destroy()
        win.createcommand('::tk::mac::ReopenApplication', onReopen)

    win.mainloop()  # wait for the user to select and click



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