#!/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 "" in the GUI to choose your account settings file "MailConfigs/mailconfig_.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('', lambda event: helpopen()) imglab.config(cursor='question_arrow') # or 'hand2'? # get account names from filenames: MailConfigs/mailconfig_.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