source: mailjam/mailjam/daemon.py@ 23:adc5b22efd7e

Last change on this file since 23:adc5b22efd7e was 23:adc5b22efd7e, checked in by Borja Lopez <borja@…>, 12 years ago

Added the first version of the CLI client, which implements a shell-like
interface where users can interact with a mailjam server.

This first versions adds no "big" features, but contains the basis to build
a complete shell interface (including history saving support, completion,
help for commands, etc)

Added a custom configuration file (conf/mailjam-cli.conf) for the CLI
client.

Added a new mailjam.config class (CLIClientConfig) that is used by the CLI
client to load its configuration file.

Updated the package information (setup.py, MANIFEST) to include the CLI
client data.

Modified the behaviour of the XMLRPC methods registered in the *XMLRPC classes
within the daemon module. Now instead of raising exceptions directly, they
catch such exceptions, returning only a string representation of the error
to the XMLRPC client. If there is no error, a nice "ok" message is returned.

File size: 10.2 KB
Line 
1# -*- coding: utf-8 -*-
2
3"""
4The mailjam project - daemon.py
5
6This file is released under the BSD license, see LICENSE for
7more information.
8
9Francisco de Borja Lopez Rio - <borja@codigo23.net>
10Soluciones Informaticas Codigo23 S.L.U. - http://codigo23.net
11"""
12
13import os, inspect, logging
14from SimpleXMLRPCServer import SimpleXMLRPCServer, list_public_methods
15
16from mailjam import config
17from mailjam.models import Member, MailingList
18from mailjam.storage import JsonStorage as Storage
19
20
21class Mailjam():
22
23 def __init__(self, configfile=None):
24 self.configfile=configfile
25 self.storage_config = config.get_config_parameters('storage',
26 configfile)
27 self.archive_config = config.get_config_parameters('archive',
28 configfile)
29
30 # lists were the currently managed mailing lists information is going
31 # to be saved
32 self.mailings = {}
33 self.mailings_addresses = []
34
35 # the files were internal information (like active mailing lists,
36 # members, etc) is saved
37 self.dbs = {'mailings': Storage(self.storage_config['lists_db']),
38 'members': Storage(self.storage_config['members_db'])}
39
40 def save(self):
41 """
42 Save all the current managed data to disk
43 """
44 if self.mailings:
45 # Save the config file from where we can reload information about
46 # the mailing lists managed by this mailjam instance
47 self.dbs['mailings'].write(self.mailings_addresses)
48 # Save each mailing list data into its separated persistence file
49 for m in self.mailings.keys():
50 self.mailings[m].save()
51 return True
52 return False
53
54 def load(self):
55 """
56 Load all data from the storage files
57 """
58 if self.dbs['mailings'].exists():
59 # load the list of managed mailing lists
60 # FIXME: This is quite naive, we do not perform any check here after
61 # loading the data from the json file, which can be modified by
62 # untrustred users.
63 self.mailings_addresses = self.dbs['mailings'].read()
64
65 # now load all the mailing objects:
66 for address in self.mailings_addresses:
67 mailing = MailingList(name=address, address=address,
68 configfile=self.configfile)
69 mailing.load()
70 self.mailings[address] = mailing
71 return True
72 return False
73
74 def clear(self):
75 """
76 Delete all stored data from disk (useful for testing).
77 DANGER: Calling this method will remove all data from disk, leaving the
78 mailjam instance with no persistence data, if the mailjam process die,
79 before another .save() call is made, all data will be lost.
80 """
81 if self.dbs['mailings'].exists():
82 # We do not delete each mailing list file, but only the file
83 # containing the list of existing mailing lists
84 self.dbs['mailings'].delete()
85 return True
86 return False
87
88 def add_mailing_list(self, info={}):
89 """
90 Add a new mailing list to this mailjam instance. expects one parameter,
91 info, which is a dictionary that should contain, at least, the
92 following keys:
93
94 - name: (string) the name we will give to the list
95 - address: (string) the email address of the list
96 - members: (list) a list of email adddress of the list members
97
98 """
99 if not isinstance(info, dict):
100 raise TypeError(info, ' is not a valid dictionary')
101
102 if 'name' not in info.keys() or \
103 'address' not in info.keys() or \
104 'members' not in info.keys() or \
105 'configfile' not in info.keys():
106 raise ValueError(info, ' does not seem to be a valid configuration')
107
108 if info['address'] in self.mailings_addresses:
109 raise IndexError(info['address'],
110 ' has been already added to mailjam')
111
112 mailing = MailingList(info['name'], info['address'],
113 info['members'], info['configfile'])
114 self.mailings[mailing.address] = mailing
115 self.mailings_addresses.append(mailing.address)
116 # After adding new mailings, save them to disk
117 self.save()
118 return True
119
120 def add_mailing_member(self, member_addr=None, list_addr=None):
121 """
122 Add a new member for the mailing list represented by list_addr (a string
123 containing the email address of any mailing list managed by this mailjam
124 instance). member_addr is a string representing the email address of the
125 new member
126 """
127
128 if not member_addr:
129 raise ValueError(member_addr, 'missing member address')
130
131 if not list_addr:
132 raise ValueError(list_addr, 'missing list address')
133
134 if list_addr not in self.mailings_addresses:
135 # FIXME: Perhaps we should add it, perhaps not (mispelled address?)
136 raise IndexError(list_addr, ' is not a valid mailing list')
137
138 added = self.mailings[list_addr].add_member_by_address(member_addr)
139 if added:
140 self.save()
141 return added
142
143
144class MailjamXMLRPC():
145 """
146 This class is a wrapper we will use to limit the methods that will be
147 published through the XMLRPC link. Only the methods from this class
148 will be available through that link.
149
150 As we use dotted names to separate xmlrpc-exported methods into different
151 namespaces, this class contains nothing, it will be used only for
152 method-registering purposes. The MailingListXMLRPC and MemberXMLRPC classes
153 contain the actual methods that are published.
154
155 More information on this approach here:
156
157 http://www.doughellmann.com/PyMOTW/SimpleXMLRPCServer/#exposing-methods-of-objects
158 """
159
160 def __init__(self, configfile=None):
161 self.configfile = configfile
162
163 def _listMethods(self):
164 public_methods = []
165 public_methods += ['lists.'+i for i in dir(MailingListXMLRPC) \
166 if '_' not in i]
167 public_methods += ['members.'+i for i in dir(MemberXMLRPC) \
168 if '_' not in i]
169 return public_methods
170
171 def _methodHelp(self, method):
172 f = getattr(self, method)
173 return inspect.getdoc(f)
174
175
176class MailingListXMLRPC():
177 def __init__(self, configfile=None):
178 self.mailjam = Mailjam(configfile=configfile)
179 self.mailjam.load()
180 def add(self, info={}):
181 try:
182 self.mailjam.add_mailing_list(info)
183 except IndexError, e:
184 return str(e)
185 return 'Added mailing list ' + info['address']
186 def addresses(self):
187 return self.mailjam.mailings_addresses
188
189
190class MemberXMLRPC():
191 def __init__(self, configfile=None):
192 self.mailjam = Mailjam(configfile=configfile)
193 self.mailjam.load()
194 def add(self, member_addr=None, list_addr=None):
195 try:
196 self.mailjam.add_mailing_member(member_addr, list_addr)
197 except IndexError, e:
198 return str(e)
199 return 'Added member ' + member_addr + ' to ' + list_addr
200 def list(self, mailing):
201 if mailing in self.mailjam.mailings_addresses:
202 return self.mailjam.mailings[mailing].members_addresses()
203
204
205class MailjamDaemon():
206 def __init__(self, configfile=None):
207 self.configfile = configfile
208 self.config = config.get_config_parameters('xmlrpc_server', configfile)
209 self.address = self.config.get('address', 'localhost')
210 self.port = int(self.config.get('port', 9876))
211 self.logfile = self.config.get('logfile',
212 os.path.join(os.path.dirname(__file__),
213 'server.log'))
214 logging.basicConfig(filename=self.logfile, level=logging.DEBUG)
215 self.server = None
216 self.ready_to_serve = False
217
218 def create_server(self):
219 """
220 If there is no server initialized in self.server, create an instance
221 of SimpleXMLRPCServer in that attribute. If there is already a server
222 initialized there, simply return True
223 """
224 if not self.server:
225 msg = 'Creating XMLRPC server object on {}:{}'.format(self.address,
226 self.port)
227 logging.info(msg)
228 self.server = SimpleXMLRPCServer((self.address, self.port),
229 allow_none=True,
230 logRequests=False)
231 self.server.register_introspection_functions()
232 return True
233
234 def add_methods(self):
235 """
236 Check if there is an initialized server (initialize it if there is none)
237 and then register all the Mailjam public methods to be served through
238 the xml-rpc link
239
240 Once the methods are registered set self.ready_to_serve to True
241 """
242 if not self.server:
243 # ensure there is an XMLRPC server initialized
244 self.create_server()
245 msg = 'Registering public methods'
246 logging.info(msg)
247 root = MailjamXMLRPC(self.configfile)
248 root.lists = MailingListXMLRPC(self.configfile)
249 root.members = MemberXMLRPC(self.configfile)
250 self.server.register_instance(root, allow_dotted_names=True)
251 self.ready_to_serve = True
252 return self.ready_to_serve
253
254 def run(self):
255 """
256 Run the xmlrpc daemon. If self.ready_to_serve is False, call
257 self.add_methods, which will initialize the server and will register all
258 the public methods into that server
259 """
260 if not self.ready_to_serve:
261 self.add_methods()
262 msg = 'Starting XMLRPC server on {}:{}'.format(self.address,
263 self.port)
264 logging.info(msg)
265 try:
266 self.server.serve_forever()
267 except KeyboardInterrupt:
268 msg = 'Stopping server'
269 logging.info(msg)
Note: See TracBrowser for help on using the repository browser.