# -*- coding: utf-8 -*- import os, inspect, logging from SimpleXMLRPCServer import SimpleXMLRPCServer, list_public_methods from postman import config from postman.models import Member, MailingList from postman.storage import JsonStorage as Storage class Postman(): def __init__(self, configfile=None): self.storage_config = config.get_config_parameters('storage', configfile) self.archive_config = config.get_config_parameters('archive', configfile) # lists were the currently managed mailing lists information is going # to be saved self.mailings = {} self.mailings_addresses = [] # the files were internal information (like active mailing lists, # members, etc) is saved self.dbs = {'mailings': Storage(self.storage_config['lists_db']), 'members': Storage(self.storage_config['members_db'])} def save(self): """ Save all the current managed data to disk """ if self.mailings: # Save the config file from where we can reload information about # the mailing lists managed by this postman instance self.dbs['mailings'].write(self.mailings_addresses) # Save each mailing list data into its separated persistence file for m in self.mailings.keys(): self.mailings[m].save() return True return False def load(self): """ Load all data from the storage files """ if self.dbs['mailings'].exists(): # load the list of managed mailing lists # FIXME: This is quite naive, we do not perform any check here after # loading the data from the json file, which can be modified by # untrustred users. self.mailings_addresses = self.dbs['mailings'].read() # now load all the mailing objects: for address in self.mailings_addresses: mailing = MailingList(address, address) mailing.load() self.mailings[address] = mailing return True return False def clear(self): """ Delete all stored data from disk (useful for testing). DANGER: Calling this method will remove all data from disk, leaving the postman instance with no persistence data, if the postman process die, before another .save() call is made, all data will be lost. """ if self.dbs['mailings'].exists(): # We do not delete each mailing list file, but only the file # containing the list of existing mailing lists self.dbs['mailings'].delete() return True return False def add_mailing_list(self, info={}): """ Add a new mailing list to this postman instance. expects one parameter, info, which is a dictionary that should contain, at least, the following keys: - name: (string) the name we will give to the list - address: (string) the email address of the list - members: (list) a list of email adddress of the list members """ if not isinstance(info, dict): raise TypeError(info, ' is not a valid dictionary') if 'name' not in info.keys() or \ 'address' not in info.keys() or \ 'members' not in info.keys() or \ 'configfile' not in info.keys(): raise ValueError(info, ' does not seem to be a valid configuration') if info['address'] in self.mailings_addresses: raise IndexError(info['address'], ' has been already added to postman') mailing = MailingList(info['name'], info['address'], info['members'], info['configfile']) self.mailings[mailing.address] = mailing self.mailings_addresses.append(mailing.address) # After adding new mailings, save them to disk self.save() return True def add_mailing_member(self, member_addr=None, list_addr=None): """ Add a new member for the mailing list represented by list_addr (a string containing the email address of any mailing list managed by this postman instance). member_addr is a string representing the email address of the new member """ if not member_addr: raise ValueError(member_addr, 'missing member address') if not list_addr: raise ValueError(list_addr, 'missing list address') if list_addr not in self.mailings_addresses: # FIXME: Perhaps we should add it, perhaps not (mispelled address?) raise IndexError(list_addr, ' is not a valid mailing list') added = self.mailings[list_addr].add_member_by_address(member_addr) if added: self.save() return added class PostmanXMLRPC(): """ This class is a wrapper we will use to limit the methods that will be published through the XMLRPC link. Only the methods from this class will be available through that link. As we use dotted names to separate xmlrpc-exported methods into different namespaces, this class contains nothing, it will be used only for method-registering purposes. The MailingListXMLRPC and MemberXMLRPC classes contain the actual methods that are published. More information on this approach here: http://www.doughellmann.com/PyMOTW/SimpleXMLRPCServer/#exposing-methods-of-objects """ def _listMethods(self): public_methods = [] public_methods += ['lists.'+i for i in dir(MailingListXMLRPC) if '_' not in i] public_methods += ['members.'+i for i in dir(MemberXMLRPC) if '_' not in i] return public_methods def _methodHelp(self, method): f = getattr(self, method) return inspect.getdoc(f) class MailingListXMLRPC(): def __init__(self): self.postman = Postman() self.postman.load() def add(self, info={}): self.postman.add_mailing_list(info) def addresses(self): return self.postman.mailings_addresses class MemberXMLRPC(): def __init__(self): self.postman = Postman() self.postman.load() def add(self, member_addr=None, list_addr=None): self.postman.add_mailing_member(member_addr, list_addr) def list(self, mailing): if mailing in self.postman.mailings_addresses: return self.postman.mailings[mailing].members_addresses() class PostmanDaemon(): def __init__(self, configfile=None): self.config = config.get_config_parameters('xmlrpc_server', configfile) # FIXME: These should be loaded from a config file self.address = self.config.get('address', 'localhost') self.port = int(self.config.get('port', 9876)) self.logfile = self.config.get('logfile', os.path.join(os.path.dirname(__file__), 'server.log')) logging.basicConfig(filename=self.logfile, level=logging.DEBUG) self.server = None self.ready_to_serve = False def create_server(self): """ If there is no server initialized in self.server, create an instance of SimpleXMLRPCServer in that attribute. If there is already a server initialized there, simply return True """ if not self.server: msg = 'Creating XMLRPC server object on {}:{}'.format(self.address, self.port) logging.info(msg) self.server = SimpleXMLRPCServer((self.address, self.port), allow_none=True, logRequests=False) self.server.register_introspection_functions() return True def add_methods(self): """ Check if there is an initialized server (initialize it if there is none) and then register all the Postman public methods to be served through the xml-rpc link Once the methods are registered set self.ready_to_serve to True """ if not self.server: # ensure there is an XMLRPC server initialized self.create_server() msg = 'Registering public methods' logging.info(msg) root = PostmanXMLRPC() root.lists = MailingListXMLRPC() root.members = MemberXMLRPC() self.server.register_instance(root, allow_dotted_names=True) self.ready_to_serve = True return self.ready_to_serve def run(self): """ Run the xmlrpc daemon. If self.ready_to_serve is False, call self.add_methods, which will initialize the server and will register all the public methods into that server """ if not self.ready_to_serve: self.add_methods() msg = 'Starting XMLRPC server on {}:{}'.format(self.address, self.port) logging.info(msg) try: self.server.serve_forever() except KeyboardInterrupt: msg = 'Stopping server' logging.info(msg)