source: mailjam/postman/daemon.py@ 8:78c437dc392c

Last change on this file since 8:78c437dc392c was 7:39e2245da71c, checked in by Francisco de Borja Lopez Rio <borja@…>, 13 years ago

Added some fixes to the XMLRPC server code

Added more tests for the XMLRPC server

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