From be622d03177a2a06fcab1a83d4662c145caa298d Mon Sep 17 00:00:00 2001 From: betonme Date: Wed, 21 Mar 2012 16:51:37 +0100 Subject: [PATCH] PushService: Initial release A flexible event notification service. Never miss any free space warning, update or timer conflict. Version 0.1 Plugins: FreeSpace, IPKGUpdateNotification, RecordNotification, RecordSummary, CrashLog, DeactivatedTimers More info at: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=167779 --- pushservice/CONTROL/control | 10 + pushservice/CONTROL/postinst | 9 + pushservice/CONTROL/postrm | 4 + pushservice/src/PluginBase.py | 125 +++++++ pushservice/src/PluginModules.py | 85 +++++ pushservice/src/Plugins/CrashLog.py | 66 ++++ pushservice/src/Plugins/DeactivatedTimers.py | 68 ++++ pushservice/src/Plugins/FreeSpace.py | 73 ++++ .../src/Plugins/IPKGUpdateNotification.py | 88 +++++ pushservice/src/Plugins/RecordNotification.py | 83 +++++ pushservice/src/Plugins/RecordSummary.py | 67 ++++ pushservice/src/PushMail.py | 83 +++++ pushservice/src/PushService.py | 83 +++++ pushservice/src/PushServiceBase.py | 167 +++++++++ pushservice/src/PushServiceConfigFile.py | 87 +++++ pushservice/src/PushServiceConfigScreen.py | 352 ++++++++++++++++++ pushservice/src/__init__.py | 14 + pushservice/src/keymap.xml | 41 ++ pushservice/src/mail.py | 145 ++++++++ pushservice/src/plugin.py | 95 +++++ 20 files changed, 1745 insertions(+) create mode 100644 pushservice/CONTROL/control create mode 100644 pushservice/CONTROL/postinst create mode 100644 pushservice/CONTROL/postrm create mode 100644 pushservice/src/PluginBase.py create mode 100644 pushservice/src/PluginModules.py create mode 100644 pushservice/src/Plugins/CrashLog.py create mode 100644 pushservice/src/Plugins/DeactivatedTimers.py create mode 100644 pushservice/src/Plugins/FreeSpace.py create mode 100644 pushservice/src/Plugins/IPKGUpdateNotification.py create mode 100644 pushservice/src/Plugins/RecordNotification.py create mode 100644 pushservice/src/Plugins/RecordSummary.py create mode 100644 pushservice/src/PushMail.py create mode 100644 pushservice/src/PushService.py create mode 100644 pushservice/src/PushServiceBase.py create mode 100644 pushservice/src/PushServiceConfigFile.py create mode 100644 pushservice/src/PushServiceConfigScreen.py create mode 100644 pushservice/src/__init__.py create mode 100644 pushservice/src/keymap.xml create mode 100644 pushservice/src/mail.py create mode 100644 pushservice/src/plugin.py diff --git a/pushservice/CONTROL/control b/pushservice/CONTROL/control new file mode 100644 index 00000000..638975b0 --- /dev/null +++ b/pushservice/CONTROL/control @@ -0,0 +1,10 @@ +Package: enigma2-plugin-extensions-pushservice +Version: 0.1 +Description: Push mail service for Enigma2 +Section: extra +Priority: optional +Maintainer: betonme +Architecture: all +OE: PushService +Homepage: IHAD +Depends: enigma2 (>= 2.6git20090615), python-twisted-core, python-twisted-mail, python-twisted-names, python-twisted-protocols, python-twisted-web, python-pyopenssl diff --git a/pushservice/CONTROL/postinst b/pushservice/CONTROL/postinst new file mode 100644 index 00000000..52e7867c --- /dev/null +++ b/pushservice/CONTROL/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +echo "********************************************************" +echo "* PushService installed *" +echo "* Coded by betonme (c) 2011 *" +echo "* Support: IHAD *" +echo "* *" +echo "* Restart Enigma-2 GUI to activate the plugin *" +echo "********************************************************" +exit 0 diff --git a/pushservice/CONTROL/postrm b/pushservice/CONTROL/postrm new file mode 100644 index 00000000..18c20e45 --- /dev/null +++ b/pushservice/CONTROL/postrm @@ -0,0 +1,4 @@ +#!/bin/sh +rm -rf /usr/lib/enigma2/python/Plugins/Extensions/PushService/ +echo "Plugin removed! You should restart enigma2 now!" +exit 0 diff --git a/pushservice/src/PluginBase.py b/pushservice/src/PluginBase.py new file mode 100644 index 00000000..f5d62875 --- /dev/null +++ b/pushservice/src/PluginBase.py @@ -0,0 +1,125 @@ +from Components.config import ConfigYesNo, NoSave + +# Plugin base class +class PluginBase(object): + # You only have to overwrite the functions You need + # If You don't have to save something, You don't need getOptions / setOptions + + UniqueCounter = 0 + + ForceSingleInstance = True + + def __init__(self): + # Is called on instance creation + + # Default configuration + self.enable = NoSave(ConfigYesNo( default = False )) + + PluginBase.UniqueCounter += 1 + self.uniqueid = PluginBase.UniqueCounter + + #self.activation = eTimer() + + self.options = {} + # Build a list of key-value string tuples + # [ (key, value, description, config element) , ] + #self.options['enabled'] = ConfigYesNo( default = False ) + + ################################################ + # Base class functions + def getName(self): + # Return the Plugin Name + return self.__class__.__name__ + + def getNameId(self): + return self.getName() + " (" + str(self.getUniqueID()) + ")" + + def getValue(self, key): + if key in self.options: + return self.options[key][0].value + else: + return None + + def setValue(self, key, value): + self.options[key][0].value = value + + def getOption(self, key): + if key in self.options: + return self.options[key] + else: + return None + + def setOption(self, key, option, description): + self.options[key] = ( option, description ) + + def setOptions(self, options): + # Parse a list of key-value string tuples + # [ (key, value) , ] + # If something is missing, the last/default value is used + for key, value in options: + default = self.getValue(key) + if type(default) is str: + self.setValue(key, value) + elif type(default) is bool: + self.setValue(key, eval(value)) + elif type(default) is int: + self.setValue(key, int(value)) + + def getUniqueID(self): + return self.uniqueid + + def getEnable(self): + return self.enable.value + + def setEnable(self, enable): + self.enable.value = enable + + def getConfigEnable(self): + return self.enable + + def getStringEnable(self): + return str(self.enable.value) + + def getStringOptions(self): + return [ ( str(key), str(option.value), str(description) ) for ( key, ( option, description ) ) in self.options.items() ] + + def getConfigOptions(self): + return [ ( key, option, description) for ( key, ( option, description ) ) in self.options.items() ] + + @classmethod + def getPluginClass(cls): + # Return the Plugin Class + return cls + + @classmethod + def forceSingle(cls): + return cls.ForceSingleInstance + + @staticmethod + def resetUniqueID(): + PluginBase.UniqueCounter = 0 + + ################################################ + # Functions to be implemented in the plugin + + def begin(self): + # Is called after starting PushSerive + pass + + def run(self): + # Return Header, Body, Attachment + # If empty or none is returned, nothing will be sent + return [], [], [] + + def end(self): + # Is called after stopping PushSerive + pass + + # Callback functions + def success(self): + # Called after successful sending the message + pass + + def error(self): + # Called after message sent has failed + pass diff --git a/pushservice/src/PluginModules.py b/pushservice/src/PluginModules.py new file mode 100644 index 00000000..cc4eecda --- /dev/null +++ b/pushservice/src/PluginModules.py @@ -0,0 +1,85 @@ +# Plugin framework +import os, imp, sys, traceback + +# Path +from Tools.Directories import resolveFilename, SCOPE_PLUGINS + +# Plugin internal +from . import _ +from PluginBase import PluginBase + + +# Constants +PLUGIN_PATH = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/PushService/Plugins/" ) +MODULE_PREFIX = 'PushService' + + +class PluginModules(object): + + def __init__(self): + self.modules = {} + + self.loadModules() + + ####################################################### + # Module functions + def loadModules(self, path = PLUGIN_PATH): + self.modules = {} + + if not os.path.exists(path): + return + + files = [fname[:-3] for fname in os.listdir(path) if fname.endswith(".py")] + for name in files: + module = None + + try: + fp, pathname, description = imp.find_module(name, [path]) + except Exception, e: + print _("[PushService] Find: ") + str(e) + fp = None + + if not fp: + print _("[PushService] Load: no module") + continue + + try: + # Use a prefix to avoid namespace conflicts + module = imp.load_module(MODULE_PREFIX+name, fp, pathname, description) + except Exception, e: + print _("[PushService] Load: ") + str(e) + finally: + # Since we may exit via an exception, close fp explicitly. + if fp: + fp.close() + + if not module: + continue + + # Instantiate only if the class is available + if not hasattr(module, name): + print _("[PushService] Warning no class definition") + continue + + # Instantiate only if the class is a subclass of PluginBase + if not issubclass( getattr(module, name), PluginBase): + print _("[PushService] Warning no subclass of PluginBase") + continue + + # Add module to the module list + self.modules[name] = getattr(module, name) + + def instantiatePlugin(self, name): + plugin = self.modules.get(name) + if plugin and callable(plugin): + # Create plugin instance + try: + return plugin() + except Exception, e: + print _("[PushService] Instantiate: ") + name + "\n" + str(e) + if sys.exc_info()[0]: + print _("Unexpected error: "), sys.exc_info()[0] + traceback.print_exc(file=sys.stdout) + else: + print _("[PushService] Module is not callable") + return None diff --git a/pushservice/src/Plugins/CrashLog.py b/pushservice/src/Plugins/CrashLog.py new file mode 100644 index 00000000..4041490b --- /dev/null +++ b/pushservice/src/Plugins/CrashLog.py @@ -0,0 +1,66 @@ +# Config +from Components.config import ConfigYesNo, NoSave + +# Plugin internal +from Plugins.Extensions.PushService.__init__ import _ +from Plugins.Extensions.PushService.PluginBase import PluginBase + +# Plugin specific +import os + + +CRASHLOG_DIR = '/media/hdd' + +SUBJECT = _("Found CrashLog(s)") +BODY = _("Crashlog(s) are attached") + + +class CrashLog(PluginBase): + + ForceSingleInstance = True + + def __init__(self): + # Is called on instance creation + PluginBase.__init__(self) + self.crashlogs = [] + + # Default configuration + self.setOption( 'delete_logs', NoSave(ConfigYesNo( default = False )), _("Delete crashlog(s)") ) + + def run(self): + # Return Header, Body, Attachment + # If empty or none is returned, nothing will be sent + # Search crashlog files + self.crashlogs = [] + text = "Found crashlogs, see attachment(s)\n" + for file in os.listdir( CRASHLOG_DIR ): + if file.startswith("enigma2_crash_") and file.endswith(".log"): + crashlog = os.path.join( CRASHLOG_DIR, file ) + self.crashlogs.append(crashlog) + if self.crashlogs: + return SUBJECT, BODY, self.crashlogs + else: + return None + + # Callback functions + def success(self): + # Called after successful sending the message + if self.getValue('delete_logs'): + # Delete crashlogs + for crashlog in self.crashlogs[:]: + if os.path.exists( crashlog ): + os.remove( crashlog ) + self.crashlogs.remove( crashlog ) + else: + # Rename crashlogs to avoid resending it + for crashlog in self.crashlogs[:]: + if os.path.exists( crashlog ): + # Adapted from autosubmit - instead of .sent we will use .pushed + currfilename = str(os.path.basename(crashlog)) + newfilename = "/media/hdd/" + currfilename + ".pushed" + os.rename(crashlog,newfilename) + self.crashlogs.remove( crashlog ) + + def error(self): + # Called after message sent has failed + self.crashlogs = [] diff --git a/pushservice/src/Plugins/DeactivatedTimers.py b/pushservice/src/Plugins/DeactivatedTimers.py new file mode 100644 index 00000000..9153f51a --- /dev/null +++ b/pushservice/src/Plugins/DeactivatedTimers.py @@ -0,0 +1,68 @@ +# Config +from Components.config import ConfigYesNo, NoSave + +# Plugin internal +from Plugins.Extensions.PushService.__init__ import _ +from Plugins.Extensions.PushService.PluginBase import PluginBase + +# Plugin specific +import NavigationInstance +from time import localtime, strftime + +SUBJECT = _("Found deactivated timer(s)") +BODY = _("Deactivated timer list:\n%s") +TAG = _("DeactivatedTimerPushed") + + +class DeactivatedTimers(PluginBase): + + ForceSingleInstance = True + + def __init__(self): + # Is called on instance creation + PluginBase.__init__(self) + self.timers = [] + + # Default configuration + self.setOption( 'remove_timer', NoSave(ConfigYesNo( default = False )), _("Remove deactivated timer(s)") ) + + def run(self): + # Return Header, Body, Attachment + # If empty or none is returned, nothing will be sent + # Search deactivated timers + self.timers = [] + text = "" + for timer in NavigationInstance.instance.RecordTimer.timer_list + NavigationInstance.instance.RecordTimer.processed_timers: + if timer.disabled and TAG not in timer.tags: + text += str(timer.name) + " " \ + + strftime(_("%Y.%m.%d %H:%M"), localtime(timer.begin)) + " - " \ + + strftime(_("%H:%M"), localtime(timer.end)) + " " \ + + str(timer.service_ref and timer.service_ref.getServiceName() or "") \ + + "\n" + self.timers.append( timer ) + if self.timers and text: + return SUBJECT, BODY % text + else: + return None + + # Callback functions + def success(self): + # Called after successful sending the message + if self.getValue('remove_timer'): + # Remove deactivated timers + for timer in self.timers[:]: + if timer in NavigationInstance.instance.RecordTimer.processed_timers: + NavigationInstance.instance.RecordTimer.processed_timers.remove(timer) + elif timer in NavigationInstance.instance.RecordTimer.timer_list: + NavigationInstance.instance.RecordTimer.timer_list.remove(timer) + self.timers.remove(timer) + else: + # Set tag to avoid resending it + for timer in self.timers[:]: + timer.tags.append(TAG) + NavigationInstance.instance.RecordTimer.saveTimer() + self.timers.remove(timer) + + def error(self): + # Called after message sent has failed + self.timers = [] diff --git a/pushservice/src/Plugins/FreeSpace.py b/pushservice/src/Plugins/FreeSpace.py new file mode 100644 index 00000000..24b00485 --- /dev/null +++ b/pushservice/src/Plugins/FreeSpace.py @@ -0,0 +1,73 @@ +# Config +from Components.config import ConfigYesNo, ConfigText, ConfigNumber, NoSave + +# Plugin internal +from Plugins.Extensions.PushService.__init__ import _ +from Plugins.Extensions.PushService.PluginBase import PluginBase + +# Plugin specific +import os + + +SUBJECT = _("Free space warning") +BODY = _("Free disk space limit has been reached:\n") \ + + _("Path: %s\n") \ + + _("Limit: %d GB\n") \ + + _("Left: %s") + + +class FreeSpace(PluginBase): + + ForceSingleInstance = False + + def __init__(self): + # Is called on instance creation + PluginBase.__init__(self) + + # Default configuration + self.setOption( 'wakehdd', NoSave(ConfigYesNo( default = False )), _("Allow HDD wake up") ) + self.setOption( 'path', NoSave(ConfigText( default = "/media/hdd/movie", fixed_size = False )), _("Where to check free space") ) + self.setOption( 'limit', NoSave(ConfigNumber( default = 100 )), _("Free space limit in GB") ) + + def run(self): + # Return Header, Body, Attachment + # If empty or none is returned, nothing will be sent + path = self.getValue('path') + limit = self.getValue('limit') + + if not self.getValue('wakehdd'): + def mountpoint(path): + path = os.path.realpath(path) + if os.path.ismount(path) or len(path)==0: return path + return mountpoint(os.path.dirname(path)) + + # User specified to avoid HDD wakeup if it is sleeping + from Components.Harddisk import harddiskmanager + p = harddiskmanager.getPartitionbyMountpoint( mountpoint(path) ) + if p is not None and p.uuid is not None: + dev = harddiskmanager.getDeviceNamebyUUID(p.uuid) + if dev is not None: + hdd = harddiskmanager.getHDD(dev) + if hdd is not None: + if hdd.isSleeping(): + # Don't wake up HDD + print _("[FreeSpace] HDD is idle: ") + str(path) + return + #TODO TEST + else: + print _("[FreeSpace] TEST HDD is not idle: ") + str(path) + + # Check free space on path + if os.path.exists( path ): + stat = os.statvfs( path ) + free = ( stat.f_bavail if stat.f_bavail!=0 else stat.f_bfree ) * stat.f_bsize / 1024 / 1024 # MB + if limit > (free/1024): #GB + if free >= 10*1024: #MB + free = "%d GB" %(free/1024) + else: + free = "%d MB" %(free) + # Not enough free space + return SUBJECT, BODY % (path, limit, free) + else: + # There is enough free space + return None diff --git a/pushservice/src/Plugins/IPKGUpdateNotification.py b/pushservice/src/Plugins/IPKGUpdateNotification.py new file mode 100644 index 00000000..13c9b625 --- /dev/null +++ b/pushservice/src/Plugins/IPKGUpdateNotification.py @@ -0,0 +1,88 @@ +# Config +from Components.config import ConfigYesNo, ConfigText, ConfigNumber, NoSave + +# Plugin internal +from Plugins.Extensions.PushService.__init__ import _ +from Plugins.Extensions.PushService.PluginBase import PluginBase + +# Plugin specific +import os +from time import time +from Plugins.SystemPlugins.SoftwareManager.SoftwareTools import iSoftwareTools + + +SUBJECT = _("IPKG Update Notification") +BODY = _("There are updates available:\n%s") + + +class IPKGUpdateNotification(PluginBase): + + ForceSingleInstance = True + + def __init__(self): + # Is called on instance creation + PluginBase.__init__(self) + + # Default configuration + self.setOption( 'selfcheck', NoSave(ConfigYesNo( default = False )), _("Start update check if not done yet") ) + + def run(self): + # Return Header, Body, Attachment + # If empty or none is returned, nothing will be sent + # Adapted from Software Manager + if iSoftwareTools.lastDownloadDate is not None and iSoftwareTools.lastDownloadDate > ( time() - (24*60*60) ): + # Last refresh was within one day + updates = self.buildList() + if updates: + return SUBJECT, BODY % (updates) + else: + print "IPKGUpdateNotification run else" + if self.getValue('selfcheck'): + # Refresh package list + iSoftwareTools.startSoftwareTools(self.getUpdateInfosCB) + #TODO async + + def getUpdateInfosCB(self, retval = None): + if retval is not None: + if retval is True: + if iSoftwareTools.available_updates is not 0: + # _("There are at least ") + str(iSoftwareTools.available_updates) + _(" updates available.") + print "Updates available." + return self.buildList() + else: + # _("There are no updates available.") + print "There are no updates available." + pass + elif retval is False: + if iSoftwareTools.lastDownloadDate is None: + if iSoftwareTools.NetworkConnectionAvailable: + # _("Updatefeed not available.") + print "Updatefeed not available." + pass + else: + # _("No network connection available.") + print "No network connection available." + pass + else: + print "IPKGUpdateNotification getUpdates" + # Call update + iSoftwareTools.lastDownloadDate = time() + iSoftwareTools.list_updating = True + iSoftwareTools.getUpdates(self.getUpdateInfosCB) + #TODO async + + def buildList(self): + updates = "" + for package in iSoftwareTools.available_updatelist: + packagename = package[0] + instversion = "" + if packagename in iSoftwareTools.installed_packetlist: + instversion = iSoftwareTools.installed_packetlist[packagename] + updversion = "" + for p, v, d in iSoftwareTools.available_packetlist: + if p == packagename: + updversion = v + break + updates += packagename + " :\t" + instversion + " :\t" + updversion + "\n" + return updates + diff --git a/pushservice/src/Plugins/RecordNotification.py b/pushservice/src/Plugins/RecordNotification.py new file mode 100644 index 00000000..d9b3d720 --- /dev/null +++ b/pushservice/src/Plugins/RecordNotification.py @@ -0,0 +1,83 @@ +# Config +from Components.config import ConfigYesNo, NoSave + +# Plugin internal +from Plugins.Extensions.PushService.__init__ import _ +from Plugins.Extensions.PushService.PluginBase import PluginBase + +# Plugin specific +import NavigationInstance +from time import localtime, strftime +from enigma import eTimer + + +SUBJECT = _("Record Notification") + + +class RecordNotification(PluginBase): + + ForceSingleInstance = True + + def __init__(self): + # Is called on instance creation + PluginBase.__init__(self) + + self.forceBindRecordTimer = eTimer() + self.forceBindRecordTimer.callback.append(self.begin) + + # Default configuration + self.setOption( 'send_on_start', NoSave(ConfigYesNo( default = False )), _("Send notification on record start") ) + self.setOption( 'send_on_end', NoSave(ConfigYesNo( default = True )), _("Send notification on record end") ) + + def begin(self): + # Is called after starting PushSerive + if self.getValue('send_on_start') or self.getValue('send_on_end'): + if NavigationInstance.instance: + if self.onRecordEvent not in NavigationInstance.instance.RecordTimer.on_state_change: + # Append callback function + NavigationInstance.instance.RecordTimer.on_state_change.append(self.onRecordEvent) + else: + # Try again later + self.forceBindRecordTimer.startLongTimer(1) + else: + # Remove callback function + self.end() + + def end(self): + # Is called after stopping PushSerive + if NavigationInstance.instance: + # Remove callback function + if self.onRecordEvent in NavigationInstance.instance.RecordTimer.on_state_change: + NavigationInstance.instance.RecordTimer.on_state_change.remove(self.onRecordEvent) + + def onRecordEvent(self, timer): + text = "" + if timer.state == timer.StatePrepared: + pass + + elif timer.state == timer.StateRunning: + if self.getValue('send_on_start'): + text += _("Record started:\n") \ + + str(timer.name) + " " \ + + strftime(_("%Y.%m.%d %H:%M"), localtime(timer.begin)) + " - " \ + + strftime(_("%H:%M"), localtime(timer.end)) + " " \ + + str(timer.service_ref and timer.service_ref.getServiceName() or "") + del timer + + # Finished repeating timer will report the state StateEnded+1 or StateWaiting + else: + if self.getValue('send_on_end'): + text += _("Record finished:\n ") \ + + str(timer.name) + "\t" \ + + strftime(_("%Y.%m.%d %H:%M"), localtime(timer.begin)) + " - " \ + + strftime(_("%H:%M"), localtime(timer.end)) + "\t" \ + + str(timer.service_ref and timer.service_ref.getServiceName() or "") + del timer + + if text: + #TODO Problem test tun won't get the message + # Push mail + from Plugins.Extensions.PushService.plugin import gPushService + if gPushService: + gPushService.push(SUBJECT, text, [], self.success, self.error) + diff --git a/pushservice/src/Plugins/RecordSummary.py b/pushservice/src/Plugins/RecordSummary.py new file mode 100644 index 00000000..95a75a26 --- /dev/null +++ b/pushservice/src/Plugins/RecordSummary.py @@ -0,0 +1,67 @@ +# Config +from Components.config import ConfigYesNo, NoSave + +# Plugin internal +from Plugins.Extensions.PushService.__init__ import _ +from Plugins.Extensions.PushService.PluginBase import PluginBase + +# Plugin specific +import NavigationInstance +from time import localtime, strftime + + +SUBJECT = _("Record Summary") +BODY = _("Finished record list:\n%s") +TAG = _("FinishedTimerPushed") + + +class RecordSummary(PluginBase): + + ForceSingleInstance = True + + def __init__(self): + # Is called on instance creation + PluginBase.__init__(self) + self.timers = [] + + # Default configuration + self.setOption( 'remove_timer', NoSave(ConfigYesNo( default = False )), _("Remove finished timer(s) only after ") ) + + def run(self): + # Return Header, Body, Attachment + # If empty or none is returned, nothing will be sent + # Search finished timers + self.timers = [] + text = "" + for timer in NavigationInstance.instance.RecordTimer.processed_timers: + if not timer.disabled and TAG not in timer.tags: + text += str(timer.name) + "\t" \ + + strftime(_("%Y.%m.%d %H:%M"), localtime(timer.begin)) + " - " \ + + strftime(_("%H:%M"), localtime(timer.end)) + "\t" \ + + str(timer.service_ref and timer.service_ref.getServiceName() or "") \ + + "\n" + self.timers.append( timer ) + if self.timers and text: + return SUBJECT, BODY % text + else: + return None + + # Callback functions + def success(self): + # Called after successful sending the message + if self.getValue('remove_timer'): + # Remove finished timers + for timer in self.timers[:]: + if timer in NavigationInstance.instance.RecordTimer.processed_timers: + NavigationInstance.instance.RecordTimer.processed_timers.remove(timer) + self.timers.remove(timer) + else: + # Set tag to avoid resending it + for timer in self.timers[:]: + timer.tags.append(TAG) + NavigationInstance.instance.RecordTimer.saveTimer() + self.timers.remove(timer) + + def error(self): + # Called after message sent has failed + self.timers = [] diff --git a/pushservice/src/PushMail.py b/pushservice/src/PushMail.py new file mode 100644 index 00000000..d3e4775d --- /dev/null +++ b/pushservice/src/PushMail.py @@ -0,0 +1,83 @@ +''' +Created on 14.11.2011 + +@author: Frank Glaser +''' + +import os +import inspect + +from Components.config import * + +# Plugin internal +from . import _ +from mail import Message, sendmail + + +# Constants +MAIL_HEADER_PREFIX = _("%s PushService: ") +MAIL_BODY_PREFIX = "" +MAIL_BODY_SUFFIX = "\n\n" \ + + _("Provided by Dreambox Plugin %s %s") + "\n" \ + + _("C 2012 by betonme @ IHAD") + "\n" \ + + _("Support %s") + "\n" \ + + _("Donate %s") + + +class PushMail(object): + def __init__(self): + pass + + def push(self, subject, body="", attachments=[], success=None, error=None, timeout=30): + from plugin import NAME, VERSION, SUPPORT, DONATE + + from_addr = config.pushservice.mailfrom.value + to_addrs = [config.pushservice.mailfrom.value or config.pushservice.mailto.value] + + # Set SMTP parameters + mailconf = {} + mailconf["host"] = config.pushservice.smtpserver.value + mailconf["port"] = config.pushservice.smtpport.value + mailconf["username"] = config.pushservice.username.value + mailconf["password"] = config.pushservice.password.value + mailconf["tls"] = config.pushservice.smtptyp.value + mailconf["timeout"] = timeout + + # Create message object + subject = ( MAIL_HEADER_PREFIX % config.pushservice.boxname.value + str(subject) ) + body = ( MAIL_BODY_PREFIX + str(body) + MAIL_BODY_SUFFIX % ( NAME, VERSION, SUPPORT, DONATE) ) + message = Message(from_addr, to_addrs, subject, body) #TODO change mime="text/plain", charset="utf-8") + if attachments: + for attachment in attachments: + message.attach(attachment) #TODO change mime=None, charset=None, content=None): + + # Send message + print _("[PushService] PushMail: Sending message: %s") % subject + deferred, connector = sendmail(mailconf, message) + + # Define callbacks + def callback(r): + print _("[PushService] PushMail: Sent successfully: %s") % subject + if callable(success): + # Check number of arguments + argspec = inspect.getargspec(success) + if len(argspec.args) > 1: + success(r) + else: + success() + + def errback(e): + print _("[PushService] PushMail: Sent failed: %s") % subject + "\n" + str(e) + if callable(error): + # Check number of arguments + argspec = inspect.getargspec(error) + if len(argspec.args) > 1: + error(e) + else: + error() + + # Add callbacks + deferred.addCallback(callback) + deferred.addErrback(errback) + + return connector diff --git a/pushservice/src/PushService.py b/pushservice/src/PushService.py new file mode 100644 index 00000000..02d364a4 --- /dev/null +++ b/pushservice/src/PushService.py @@ -0,0 +1,83 @@ +''' +Created on 06.11.2011 + +@author: Frank Glaser +''' + + +import os +import sys, traceback + +from Components.config import * +from enigma import eTimer +from time import localtime, strftime + + +# Plugin internal +from . import _ +from PushServiceBase import PushServiceBase + + +####################################################### +# Logical part +class PushService(PushServiceBase): + + def __init__(self): + PushServiceBase.__init__(self) + + self.plugins = [] + + self.state = "First" + self.timer = eTimer() + self.timer.callback.append(self.go) + + def start(self, state = None): + if self.timer.isActive(): + self.timer.stop() + + # Read XML file, parse it and instantiate configured plugins + plugins = self.load() + if plugins: + self.plugins = plugins + + self.begin(self.plugins) + + self.next(state) + + def next(self, state = None): + #TODO Start run in a new thread !!!!!!!!! + # Override statemachine + if state: self.state = state + + if self.state == "Now": + self.state = "First" + self.go() + + if self.state == "Boot": + self.state = "First" + self.timer.startLongTimer( 10 ) + + elif self.state == "First": + self.state = "Period" + cltime = config.pushservice.time.value + lotime = localtime() + ltime = lotime[3]*60 + lotime[4] + ctime = cltime[0]*60 + cltime[1] + seconds = 60 * abs(ctime - ltime) + self.timer.startLongTimer( seconds ) + + elif self.state == "Period": + period = config.pushservice.period.value + if period > 0: + self.timer.startLongTimer( int(period)*60*60 ) + + def stop(self): + # Stop Timer + if self.timer.isActive(): + self.timer.stop() + self.state = "First" + self.end(self.plugins) + + def go(self): + self.run(self.plugins) + self.next() diff --git a/pushservice/src/PushServiceBase.py b/pushservice/src/PushServiceBase.py new file mode 100644 index 00000000..f8a7298d --- /dev/null +++ b/pushservice/src/PushServiceBase.py @@ -0,0 +1,167 @@ +import os + +# Config +from Components.config import * + +# XML +from xml.etree.cElementTree import Element, SubElement, Comment +from Tools.XMLTools import stringToXML + +# Plugin internal +from . import _ +from PushMail import PushMail +from PluginModules import PluginModules +from PushServiceConfigFile import PushServiceConfigFile +from PluginBase import PluginBase + + +# Constants +PLUGIN = "Plugin" +OPTION = "Option" + + +class PushServiceBase(PushMail, PluginModules, PushServiceConfigFile): + + def __init__(self, path=""): + PushMail.__init__(self) + PluginModules.__init__(self) + PushServiceConfigFile.__init__(self) + + + ###################################### + # Plugin handling + def begin(self, plugins): + # Loop over all Plugins + if plugins: + for plugin in self.plugins: + if plugin.getEnable(): + plugin.begin() + + def end(self, plugins): + # Loop over all Plugins + if plugins: + for plugin in self.plugins: + if plugin.getEnable(): + plugin.end() + + def run(self, plugins, send=True): + response = "" + print _("PushService started: ") + strftime( _("%d.%m.%Y %H:%M"), localtime() ) + + # Loop over all Plugins + if plugins: + for plugin in plugins: + if plugin.getEnable(): + print _("PushService running: ") + str( plugin.getName() ) + subject, text, attachments = "", "", [] + + try: + # Run plugin + ret = plugin.run() + except Exception, e: + print _("PushService run exception ") + str(e) + if sys.exc_info()[0]: + print _("Unexpected error: "), sys.exc_info()[0] + traceback.print_exc(file=sys.stdout) + ret = None + + # Parse return value(s) + if ret: + if len(ret) == 3: + subject, text, attachments = ret + elif len(ret) == 2: + # No attachment given + subject, text = ret + attachments = [] + else: + # Header and Body will contain the same + subject = text = ret + attachments = [] + + # Prepare resonse text + if subject: response += "[ " + subject + " ]\n\n" + if text: response += text + "\n\n\n" + + if send and subject: + # Push mail + self.push(subject, text, attachments, plugin.success, plugin.error) + return response or "Nothing to send" + + + ###################################### + # Config + def load(self): + path = config.pushservice.xmlpath.value + # Read xml config file + root = self.readXML( path ) + if root: + # Reset the unique id counter + PluginBase.resetUniqueID() + return self.parseConfig( root ) + else: + return [] + + def save(self, plugins): + path = config.pushservice.xmlpath.value + root = self.buildConfig( plugins ) + self.writeXML( root, path ) + + + ###################################### + # Internal + def parseConfig(self, root): + plugins = [] + + #if version != CURRENT_CONFIG_VERSION: + # parseConfigOld(configuration, list, uniqueTimerId) + # return + + if root: + from xml.etree.cElementTree import tostring + for element in root.findall(PLUGIN): + name = element.get("name", "") + enable = element.get("enable", "True") + if name: + plugin = self.instantiatePlugin(name) + if plugin: + plugin.setEnable(eval(enable)) + + # Set plugin options + options = [] + for option in element.findall(OPTION): + key = option.get("key", "") + value = option.text + if key and value: + options.append((key, value)) + + if options: + plugin.setOptions(options) + + print "PLUGIN APPEND" + + # Append to active plugin list + plugins.append(plugin) + return plugins + + def buildConfig(self, plugins): + # Generate List in RAM + from plugin import NAME, VERSION + + # Header + root = Element('PushService') + root.set('version', VERSION) + root.append(Comment(_("Don't edit this manually unless you really know what you are doing"))) + + # Body + if plugins: + for plugin in plugins: + # Add plugin + element = SubElement(root, PLUGIN, name=stringToXML(plugin.getName()), enable=stringToXML(plugin.getStringEnable())) + + # Add options + options = plugin.getStringOptions() + if options: + for key, value, description in options: + SubElement( element, OPTION, key = stringToXML(key) ).text = stringToXML(value) + + return root diff --git a/pushservice/src/PushServiceConfigFile.py b/pushservice/src/PushServiceConfigFile.py new file mode 100644 index 00000000..cf654ee0 --- /dev/null +++ b/pushservice/src/PushServiceConfigFile.py @@ -0,0 +1,87 @@ +import os + +# XML +from xml.etree.cElementTree import ElementTree, tostring, parse + +# Plugin internal +from . import _ +from PluginModules import PluginModules + + +def indent(elem, level=0): + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class PushServiceConfigFile(object): + + def __init__(self): + self.path = "" + self.mtime = -1 + self.cache = "" + + def __setPath(self, path): + if self.path != path: + self.path = path + self.mtime = -1 + self.cache = "" + + def readXML(self, path): + self.__setPath(path) + + # Abort if no config found + if not os.path.exists(path): + print _("[PushService] No configuration file present") + return None + + # Parse if mtime differs from whats saved + mtime = os.path.getmtime(path) + if mtime == self.mtime: + # No changes in configuration, won't read again + return self.cache + + # Parse XML + try: + etree = parse(path).getroot() + except Exception, e: + print _("[PushService] Exception in readXML: ") + str(e) + etree = None + mtime = -1 + + # Save time and cache file content + self.mtime = mtime + self.cache = etree + return self.cache + + def writeXML(self, etree, path): + self.__setPath(path) + + indent(etree) + data = tostring(etree, 'utf-8') + + f = None + try: + f = open(path, 'w') + if data: + f.writelines(data) + except Exception, e: + print _("[PushService] Exception in writeXML: ") + str(e) + finally: + if f is not None: + f.close() + + # Save time and cache file content + self.mtime = os.path.getmtime( path ) + self.cache = etree + diff --git a/pushservice/src/PushServiceConfigScreen.py b/pushservice/src/PushServiceConfigScreen.py new file mode 100644 index 00000000..1b5132d9 --- /dev/null +++ b/pushservice/src/PushServiceConfigScreen.py @@ -0,0 +1,352 @@ +''' +Created on 06.11.2011 + +@author: Frank Glaser +''' + +# Config +from Components.config import * +from Components.ConfigList import ConfigListScreen +from Components.Sources.StaticText import StaticText + +# Screen +from Components.ActionMap import ActionMap +from Components.ActionMap import HelpableActionMap +from Components.ScrollLabel import ScrollLabel +from enigma import eSize, ePoint, getDesktop +from Screens.Screen import Screen +from Screens.Setup import SetupSummary +from Screens.ChoiceBox import ChoiceBox +from Screens.MessageBox import MessageBox +from Screens.HelpMenu import HelpableScreen + +# Plugin internal +from . import _ +from PushService import PushService + + +# Constants +separator = "".ljust(250,"-") + + +# TODO Everytime the user will enter the ConfigurationScreen: +# Modules will be reloaded +# TODO Everytime the user will leave the ConfigurationScreen: +# Plugins will be reloaded + +####################################################### +# Configuration screen +class PushServiceConfigScreen(Screen, ConfigListScreen, HelpableScreen): + + skin = """ + + + + + + + + + + + + + """ +# + + def __init__(self, session): + #TODO + try: + Screen.__init__(self, session) + HelpableScreen.__init__(self) + self.skinName = ["PushServiceConfigScreen", "ConfigListScreen"] + + # Summary + from plugin import NAME, VERSION, gPushService + self.setup_title = NAME + " " + _("Configuration") + " " + VERSION + + if gPushService: + # Save PushService instance + self.pushservice = gPushService + # Stop PushService + self.pushservice.stop() + else: + # PushService not running - Instantiate a new one + global gPushService + self.pushservice = PushService() + + # Load local plugins to work on + self.plugins = self.pushservice.load() + + # Buttons + self["key_red"] = StaticText(_("Cancel")) + self["key_green"] = StaticText(_("OK")) + self["key_blue"] = StaticText(_("Add plugin")) + self["key_yellow"] = StaticText(_("Remove plugin")) + #TODO + #self["key_info"] test mail + #self["key_play"] test run + + #TODO Maybe LATER + #self["help"] = StaticText() + #self["HelpWindow"].hide() + #self["VirtualKB"].setEnabled(False) + #self["VKeyIcon"].boolean = False + self.help_window = None + + # Define Actions + #Bug self["custom_actions"] = HelpableActionMap(self, ["SetupActions", "ColorActions", "PushServiceConfigActions"], + self["custom_actions"] = HelpableActionMap(self, "PushServiceConfigActions", + { + "cancel": (self.keyCancel, _("Exit without saving")), + "save": (self.keySave, _("Save and exit.")), + "blue": (self.addPlugin, _("Add plugin")), + "yellow": (self.removePlugin, _("Remove plugin")), + "pageUp": (self.pageUp, _("Page up")), + "pageDown": (self.pageDown, _("Page down")), + "testMail": (self.testMail, _("Send a test mail")), + "runNow": (self.runNow, _("Test run")), + }, -2) # higher priority + + # Initialize Configuration part + self.list = [] + ConfigListScreen.__init__(self, self.list, session = session, on_change = self.buildConfig) + self.buildConfig() + + # Override selectionChanged because our config tuples are bigger + self.onChangedEntry = [ ] + def selectionChanged(): + current = self["config"].getCurrent() + if self["config"].current != current: + if self["config"].current: + self["config"].current[1].onDeselect(self.session) + if current: + current[1].onSelect(self.session) + self["config"].current = current + for x in self["config"].onSelectionChanged: + x() + self["config"].selectionChanged = selectionChanged + + self.setTitle(self.setup_title) + except Exception, e: + print "PushServiceConfigScreen init exception " + str(e) + + def buildConfig(self, selectuniqueid=None): + #TODO + try: + self.list = [] + select = None + lappend = self.list.append + + lappend( getConfigListEntry( _("Enable PushService"), config.pushservice.enable, 0 ) ) + + if config.pushservice.enable.value: + lappend( getConfigListEntry( _("Dreambox name"), config.pushservice.boxname, 0 ) ) + lappend( getConfigListEntry( _("Config file"), config.pushservice.xmlpath, 0 ) ) + + lappend( getConfigListEntry( _("Start time (HH:MM)"), config.pushservice.time, 0 ) ) + lappend( getConfigListEntry( _("Period in hours (0=disabled)"), config.pushservice.period, 0 ) ) + lappend( getConfigListEntry( _("Run on boot"), config.pushservice.runonboot, 0 ) ) + + lappend( getConfigListEntry( _("SMTP Server"), config.pushservice.smtpserver, 0 ) ) + lappend( getConfigListEntry( _("SMTP Port"), config.pushservice.smtpport, 0 ) ) + lappend( getConfigListEntry( _("SMTP SSL"), config.pushservice.smtptyp, 0 ) ) + lappend( getConfigListEntry( _("User name"), config.pushservice.username, 0 ) ) + lappend( getConfigListEntry( _("Password"), config.pushservice.password, 0 ) ) + lappend( getConfigListEntry( _("Mail from"), config.pushservice.mailfrom, 0 ) ) + lappend( getConfigListEntry( _("Mail to or leave empty"), config.pushservice.mailto, 0 ) ) + + if self.plugins: + lappend( getConfigListEntry( separator, config.pushservice.about, 0 ) ) + + for idx, plugin in enumerate(self.plugins): + lappend( getConfigListEntry( plugin.getNameId(), plugin.getConfigEnable(), idx ) ) + if plugin.getUniqueID() == selectuniqueid: + # Select the added plugin + select = len(self.list)-1 + if plugin.getEnable(): + for key, element, description in plugin.getConfigOptions(): + lappend( getConfigListEntry( " " + str(description), element, idx ) ) + + self["config"].setList( self.list ) + del lappend + + if select is not None: + self["config"].instance.moveSelectionTo(select) + + except Exception, e: + print _("PushServiceConfigScreen build exception ") + str(e) + + def addPlugin(self): + self.hideHelpWindow() + addlist = [] + pluginclasslist = [] + if self.plugins: + pluginclasslist = [ plg.getPluginClass() for plg in self.plugins] + for name, module in self.pushservice.modules.iteritems(): + if module.forceSingle(): + # We have to check if there is already a plugin instance + if module in pluginclasslist: + # A plugin instance already exists + continue + addlist.append( (name, name) ) + addlist.sort() + self.session.openWithCallback(self.addPluginCB, ChoiceBox,_("Add plugin"), addlist) + + def addPluginCB(self, result): + name = result and result[1] + if name: + plugin = self.pushservice.instantiatePlugin( name ) + if plugin: + plugin.setEnable(True) + + self.plugins.append( plugin ) + self.plugins.sort( key=lambda x: ( x.getUniqueID() ) ) + + self.buildConfig( plugin.getUniqueID() ) + + def removePlugin(self): + self.hideHelpWindow() + if self.plugins: + select = 0 + current = self["config"].getCurrent() + if current: + select = current[2] + plist = [] + if self.plugins: + plist = [( plugin.getNameId(), plugin ) for plugin in self.plugins ] + self.session.openWithCallback(self.removePluginCB, ChoiceBox,_("Remove plugin"), list=plist, selection=select) + + def removePluginCB(self, result): + plugin = result and result[1] + if plugin: + self.plugins.remove( plugin ) + self.buildConfig() + + # Overwrite ConfigListScreen keySave function + def keySave(self): + self.hideHelpWindow() + + # Save E2 PushService config + self.saveAll() + + # Build xml config and write it + self.pushservice.save(self.plugins) + + from plugin import gPushService + global gPushService + if config.pushservice.enable.value: + gPushService = self.pushservice + #TODO gPushService.load() + gPushService.start() #with load + else: + gPushService = None + + self.close() + + # Overwrite ConfigListScreen keyCancel function + def keyCancel(self): + self.hideHelpWindow() + # Call baseclass function + ConfigListScreen.keyCancel(self) + + # Overwrite ConfigListScreen cancelConfirm function + def cancelConfirm(self, result): + from plugin import gPushService + if gPushService: + # Start PushService + self.pushservice.start() + + # Call baseclass function + ConfigListScreen.cancelConfirm(self, result) + + # Overwrite Screen close function + def close(self): + self.hideHelpWindow() + from plugin import ABOUT + self.session.openWithCallback(self.closeConfirm, MessageBox, ABOUT, MessageBox.TYPE_INFO) + + def closeConfirm(self, dummy=None): + # Call baseclass function + Screen.close(self) + + def getCurrentEntry(self): + return self["config"].getCurrent()[0] + + def getCurrentValue(self): + return str(self["config"].getCurrent()[1].getText()) + + def createSummary(self): + return SetupSummary + + def pageUp(self): + self["config"].instance.moveSelection(self["config"].instance.pageUp) + + def pageDown(self): + self["config"].instance.moveSelection(self["config"].instance.pageDown) + + def testMail(self): + self.hideHelpWindow() + self.testMailBox = None + timeout = 30 + # Send test mail + connector = self.pushservice.push(_("Test mail"), _("If You can see this, Your SMTP configuration is correct."), [], self.success, self.error, timeout=timeout) + def testMailCB(result): + connector.disconnect() + self.testMailBox = self.session.openWithCallback(testMailCB, TestMailBox, _("Testing SMTP"), _("Sending test mail...\n\nCancel?"), MessageBox.TYPE_INFO, timeout=timeout) + def success(self): + if self.testMailBox: + self.testMailBox.setText(_("The mail has been sent successfully")) + def error(self, e): + if self.testMailBox: + self.testMailBox.setText(_("Mail sent failed:\n\n%s") % e.getErrorMessage()) + self.testMailBox.setText(_("Mail sent failed:\n\n%s\n\n%s") % (e.type, e.value)) + + def runNow(self): + self.hideHelpWindow() + # Test actually not saved configuration + response = self.pushservice.run(self.plugins, False) + self.session.open(TestRunConsole, _("Test run"), response) + + def hideHelpWindow(self): + current = self["config"].getCurrent() + if current and hasattr(current[1], "help_window"): + help_window = current[1].help_window + if help_window: + help_window.hide() + + +class TestMailBox(MessageBox): + def __init__(self, session, title, text, type = MessageBox.TYPE_YESNO, timeout = -1, close_on_any_key = False, default = True, enable_input = True, msgBoxID = None): + MessageBox.__init__(self, session, text, type, timeout, close_on_any_key, default, enable_input, msgBoxID) + self.skinName = ["TestMailBox", "MessageBox"] + self.setTitle(title) + + def setText(self, text): + self.stopTimer() + self["text"].setText(text) + #self.resize() + self.createGUIScreen(self.instance, self.desktop, updateonly = True) + + +class TestRunConsole(Screen): + def __init__(self, session, title = "Console", text = ""): + Screen.__init__(self, session) + self.skinName = ["TestMailBox", "Console"] + + self["text"] = ScrollLabel("") + self["actions"] = ActionMap(["WizardActions", "DirectionActions"], + { + "ok": self.cancel, + "back": self.cancel, + "up": self["text"].pageUp, + "down": self["text"].pageDown + }, -1) + self.setTitle(title) + self.setText(text) + + def setText(self, text): + self["text"].setText(text) + + def cancel(self): + self.close() diff --git a/pushservice/src/__init__.py b/pushservice/src/__init__.py new file mode 100644 index 00000000..97e1e4cd --- /dev/null +++ b/pushservice/src/__init__.py @@ -0,0 +1,14 @@ +from Components.Language import language +from Tools.Directories import resolveFilename, SCOPE_PLUGINS, SCOPE_LANGUAGE +from os import environ as os_environ +import gettext + +def localeInit(): + lang = language.getLanguage()[:2] # getLanguage returns e.g. "fi_FI" for "language_country" + os_environ["LANGUAGE"] = lang # Enigma doesn't set this (or LC_ALL, LC_MESSAGES, LANG). gettext needs it! + gettext.bindtextdomain("PS", resolveFilename(SCOPE_PLUGINS, "Extensions/PushService/locale")) + +_ = lambda txt: gettext.dgettext("PS", txt) if txt else "" + +localeInit() +language.addCallback(localeInit) diff --git a/pushservice/src/keymap.xml b/pushservice/src/keymap.xml new file mode 100644 index 00000000..99b4aaf0 --- /dev/null +++ b/pushservice/src/keymap.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pushservice/src/mail.py b/pushservice/src/mail.py new file mode 100644 index 00000000..38fd9d90 --- /dev/null +++ b/pushservice/src/mail.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# coding: utf-8 +# +# Copyright 2010 Alexandre Fiori +# based on the original Tornado by Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# Changes: +# 24.02.2012 betonme +# Added retries and timeout parameter +# Return deferred and connector + +"""Implementation of e-mail Message and SMTP with and without SSL""" + +import types +import os.path +from cStringIO import StringIO +from OpenSSL.SSL import SSLv3_METHOD + +from email import Encoders +from email.MIMEText import MIMEText +from email.MIMEBase import MIMEBase +from email.MIMEMultipart import MIMEMultipart +from email.Utils import COMMASPACE, formatdate + +from twisted.internet import reactor +from twisted.internet.defer import Deferred +from twisted.internet.ssl import ClientContextFactory +from twisted.mail.smtp import ESMTPSenderFactory + +class Message(object): + def __init__(self, from_addr, to_addrs, subject, message, mime="text/plain", charset="utf-8"): + self.subject = subject + self.from_addr = from_addr + self.to_addrs = isinstance(to_addrs, types.StringType) and [to_addrs] or to_addrs + + self.msg = None + self.__cache = None + self.message = MIMEText(message) + self.message.set_charset(charset) + self.message.set_type(mime) + + def attach(self, filename, mime=None, charset=None, content=None): + base = os.path.basename(filename) + if content is None: + fd = open(filename) + content = fd.read() + fd.close() + + if not isinstance(content, types.StringType): + raise TypeError("don't know how to handle content: %s" % type(content)) + + part = MIMEBase("application", "octet-stream") + part.set_payload(content) + Encoders.encode_base64(part) + part.add_header("Content-Disposition", "attachment; filename=\"%s\"" % base) + + if mime is not None: + part.set_type(mime) + + if charset is not None: + part.set_charset(charset) + + if self.msg is None: + self.msg = MIMEMultipart() + self.msg.attach(self.message) + + self.msg.attach(part) + + def __str__(self): + return self.__cache or "nuswit mail message: not rendered yet" + + def render(self): + if self.msg is None: + self.msg = self.message + + self.msg["Subject"] = self.subject + self.msg["From"] = self.from_addr + self.msg["To"] = COMMASPACE.join(self.to_addrs) + self.msg["Date"] = formatdate(localtime=True) + + if self.__cache is None: + self.__cache = self.msg.as_string() + + return StringIO(self.__cache) + + +def sendmail(mailconf, message): + """Takes a regular dictionary as mailconf, as follows: + + mailconf["host"] = "your.smtp.com" (required) + mailconf["port"] = 25 (optional, default 25 or 587 for TLS) + mailconf["username"] = "username" (optional) + mailconf["password"] = "password" (optional) + mailconf["tls"] = True | False (optional, default False) + mailconf["retries"] = 0 (optional, default 0) + mailconf["timeout"] = 30 (optional, default 30) + """ + if not isinstance(mailconf, types.DictType): + raise TypeError("mailconf must be a regular python dictionary") + + if not isinstance(message, Message): + raise TypeError("message must be an instance of nuswit.mail.Message") + + host = mailconf.get("host") + if not isinstance(host, types.StringType): + raise ValueError("mailconf requires a 'host' configuration") + + if mailconf.get("tls", False) is True: + port = mailconf.get("port", 587) + contextFactory = ClientContextFactory() + contextFactory.method = SSLv3_METHOD + else: + port = mailconf.get("port", 25) + contextFactory = None + + retries = mailconf.get("retries", 0) + timeout = mailconf.get("timeout", 30) + + if not isinstance(port, types.IntType): + raise ValueError("mailconf requires a proper 'port' configuration") + + deferred = Deferred() + username, password = mailconf.get("username"), mailconf.get("password") + factory = ESMTPSenderFactory( + username, password, + message.from_addr, message.to_addrs, message.render(), + deferred, contextFactory=contextFactory, + requireAuthentication=(username and password), + retries=retries, timeout=timeout) + + connector = reactor.connectTCP(host, port, factory, timeout=timeout) + + return deferred, connector diff --git a/pushservice/src/plugin.py b/pushservice/src/plugin.py new file mode 100644 index 00000000..9a973ebd --- /dev/null +++ b/pushservice/src/plugin.py @@ -0,0 +1,95 @@ +# Plugin +from Plugins.Plugin import PluginDescriptor + +# Config +from Components.config import * + +# Default encoding +#from Components.Language import language + +# Plugin internal +from __init__ import * +from PushService import PushService +from PushServiceConfigScreen import PushServiceConfigScreen +#from PluginBase import PluginBase +#from ConfigDirectorySelection import ConfigDirectorySelection + + +# Constants +NAME = "PushService" +VERSION = "0.1" +SUPPORT = "http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=167779" +DONATE = "http://bit.ly/pspaypal" +ABOUT = "\n " + NAME + " " + VERSION + "\n\n" \ + + _(" (C) 2012 by betonme @ IHAD \n\n") \ + + _(" If You like this plugin and want to support it,\n") \ + + _(" or if just want to say ''thanks'',\n") \ + + _(" feel free to donate via PayPal. \n\n") \ + + _(" Thanks a lot ! \n\n PayPal: ") + DONATE + + +# Globals +gPushService = None + + +# Config options +config.pushservice = ConfigSubsection() + +config.pushservice.about = ConfigNothing() + +config.pushservice.enable = ConfigEnableDisable(default = True) + +config.pushservice.boxname = ConfigText(default = "Dreambox", fixed_size = False) +config.pushservice.xmlpath = ConfigText(default = "/etc/enigma2/pushservice.xml", fixed_size = False) + +config.pushservice.time = ConfigClock(default = 0) +config.pushservice.period = ConfigSelectionNumber(0, 1000, 1, default = 24) +config.pushservice.runonboot = ConfigEnableDisable(default = True) + +config.pushservice.smtpserver = ConfigText(default="smtp.server.com", fixed_size = False) +config.pushservice.smtpport = ConfigNumber(default = 587) +config.pushservice.smtptyp = ConfigEnableDisable(default = True) + +config.pushservice.username = ConfigText(fixed_size = False) +config.pushservice.password = ConfigPassword() + +config.pushservice.mailfrom = ConfigText(default = "abc@provider.com", fixed_size = False) +config.pushservice.mailto = ConfigText(fixed_size = False) + + +####################################################### +# Plugin main function +def Plugins(**kwargs): + localeInit() + + descriptors = [] + + if config.pushservice.enable.value: + # AutoStart + descriptors.append( PluginDescriptor(where = PluginDescriptor.WHERE_AUTOSTART, fnc = autostart, needsRestart = False) ) + + #TODO icon + descriptors.append( PluginDescriptor(name = NAME, description = NAME + " " +_("configuration"), where = PluginDescriptor.WHERE_PLUGINMENU, fnc = setup, needsRestart = False) ) #icon = "/icon.png" + + return descriptors + + +####################################################### +# Plugin configuration +def setup(session, **kwargs): + session.open(PushServiceConfigScreen) + + +####################################################### +# Autostart +def autostart(reason, **kwargs): + if reason == 0: # start + if config.pushservice.enable.value: + global gPushService + gPushService = PushService() + #TODO gPushService.load() + state = None + if config.pushservice.runonboot.value: + state = "Boot" + gPushService.start(state) #with load + -- 2.20.1