source: mailjam/postman/daemon.py@ 15:8ae771653ffe

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

Added a postman-mta.conf version to be used in the tests

Added a script bin/postman-mta that will run the MTA client from a shell

Renamed the bin/postman_server script to bin/postman-server

Disabled the tests from tests/mta.py, as they are totally outdated and they
break the tests.

Replaced the old code in mta.Sendmail with a new mta.MTAClient class, which
contains code that actually *works* against a postman server (tests to come!)

Restructured the postman-mta.conf configuration file. The sections have better
names now (server, archive) and the archive section contains parameters which
names that reflect much better what they are used for.

Added a new parameter to the postman-mta.conf file (server.uri) that allows
us to set the uri to where any MTA client will have to connect, in only one
shorter line.

Fixed some typos in postman.config

Updated the setup.py script so the bin/postman-* scripts are installed
properly in the bin/ directory.

Added a function, proper_etc_path(), to the setup.py script so the path where
configuration files have to be installed is correctly set when building a
distribution/source package (still to be tested).

Updated the MANIFEST

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