Merge lp:~jskrzeszewska/mailman/mailman into lp:mailman
- mailman
- Merge into 3.0
Proposed by
Joanna Skrzeszewska
Status: | Needs review |
---|---|
Proposed branch: | lp:~jskrzeszewska/mailman/mailman |
Merge into: | lp:mailman |
Diff against target: |
553 lines (+513/-0) 5 files modified
src/mailman/archiving/rssarchive.py (+225/-0) src/mailman/archiving/tests/test_rssarchive.py (+203/-0) src/mailman/config/mailman.cfg (+4/-0) src/mailman/rest/root.py (+9/-0) src/mailman/rest/rss.py (+72/-0) |
To merge this branch: | bzr merge lp:~jskrzeszewska/mailman/mailman |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Nicki Hutchens (community) | Approve | ||
Nicki Hutchens | Pending | ||
Barry Warsaw | Pending | ||
Review via email: mp+178414@code.launchpad.net |
Commit message
Description of the change
Added archiver for rss feed and REST API for rss settings.
To post a comment you must log in.
- 7218. By Joanna Skrzeszewska
-
Changes in rssarchive.py
- 7219. By Joanna Skrzeszewska
-
Tests for rssarchive
Unmerged revisions
- 7219. By Joanna Skrzeszewska
-
Tests for rssarchive
- 7218. By Joanna Skrzeszewska
-
Changes in rssarchive.py
- 7217. By Joanna Skrzeszewska
-
Adding REST API for rss archiver.
- 7216. By Joanna Skrzeszewska
-
Adding rss archiver.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'src/mailman/archiving/rssarchive.py' | |||
2 | --- src/mailman/archiving/rssarchive.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ src/mailman/archiving/rssarchive.py 2013-09-01 16:37:50 +0000 | |||
4 | @@ -0,0 +1,225 @@ | |||
5 | 1 | # Copyright (C) 2008-2013 by the Free Software Foundation, Inc. | ||
6 | 2 | # | ||
7 | 3 | # This file is part of GNU Mailman. | ||
8 | 4 | # | ||
9 | 5 | # GNU Mailman is free software: you can redistribute it and/or modify it under | ||
10 | 6 | # the terms of the GNU General Public License as published by the Free | ||
11 | 7 | # Software Foundation, either version 3 of the License, or (at your option) | ||
12 | 8 | # any later version. | ||
13 | 9 | # | ||
14 | 10 | # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT | ||
15 | 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
16 | 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
17 | 13 | # more details. | ||
18 | 14 | # | ||
19 | 15 | # You should have received a copy of the GNU General Public License along with | ||
20 | 16 | # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. | ||
21 | 17 | |||
22 | 18 | from __future__ import absolute_import, print_function, unicode_literals | ||
23 | 19 | |||
24 | 20 | __metaclass__ = type | ||
25 | 21 | __all__ = [ | ||
26 | 22 | 'RssArchiver', | ||
27 | 23 | ] | ||
28 | 24 | |||
29 | 25 | import os | ||
30 | 26 | import errno | ||
31 | 27 | import logging | ||
32 | 28 | import datetime | ||
33 | 29 | import PyRSS2Gen | ||
34 | 30 | import sqlite3 | ||
35 | 31 | |||
36 | 32 | from email.utils import parsedate | ||
37 | 33 | from datetime import datetime | ||
38 | 34 | from operator import itemgetter | ||
39 | 35 | from datetime import timedelta | ||
40 | 36 | from mailbox import Maildir | ||
41 | 37 | from urlparse import urljoin | ||
42 | 38 | |||
43 | 39 | from flufl.lock import Lock, TimeOutError | ||
44 | 40 | from zope.interface import implementer | ||
45 | 41 | from mailman.interfaces.listmanager import IListManager | ||
46 | 42 | from zope.component import getUtility | ||
47 | 43 | |||
48 | 44 | from mailman.config import config | ||
49 | 45 | from mailman.interfaces.archiver import IArchiver | ||
50 | 46 | |||
51 | 47 | log = logging.getLogger('mailman.error') | ||
52 | 48 | |||
53 | 49 | @implementer(IArchiver) | ||
54 | 50 | class RssArchiver: | ||
55 | 51 | name = 'rssarchive' | ||
56 | 52 | table_name = 'admin_features' | ||
57 | 53 | |||
58 | 54 | @staticmethod | ||
59 | 55 | def set_length_limit(mlist, length_limit): | ||
60 | 56 | rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db') | ||
61 | 57 | is_enabled = RssArchiver.is_feed_enabled(mlist) | ||
62 | 58 | conn = sqlite3.connect(rss_db_dir) | ||
63 | 59 | c = conn.cursor() | ||
64 | 60 | sql = ('insert or replace into ' + RssArchiver.table_name + | ||
65 | 61 | ' values (\'' + mlist.fqdn_listname + '\', ' + str(is_enabled) + | ||
66 | 62 | ', ' + str(length_limit) + ')') | ||
67 | 63 | c.execute(sql) | ||
68 | 64 | conn.commit() | ||
69 | 65 | c.close() | ||
70 | 66 | conn.close() | ||
71 | 67 | |||
72 | 68 | @staticmethod | ||
73 | 69 | def set_feed_enabled(mlist, is_enabled): | ||
74 | 70 | rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db') | ||
75 | 71 | conn = sqlite3.connect(rss_db_dir) | ||
76 | 72 | c = conn.cursor() | ||
77 | 73 | length_limit = RssArchiver.get_length_limit(mlist) | ||
78 | 74 | sql = ('insert or replace into ' + RssArchiver.table_name + | ||
79 | 75 | ' values (\'' + mlist.fqdn_listname + '\', ' + str(is_enabled) + | ||
80 | 76 | ', ' +str(length_limit) + ')') | ||
81 | 77 | c.execute(sql) | ||
82 | 78 | conn.commit() | ||
83 | 79 | c.close() | ||
84 | 80 | conn.close() | ||
85 | 81 | if is_enabled: | ||
86 | 82 | RssArchiver.generate_rss_feed(mlist) | ||
87 | 83 | |||
88 | 84 | @staticmethod | ||
89 | 85 | def get_length_limit(mlist): | ||
90 | 86 | rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db') | ||
91 | 87 | RssArchiver.init_database() | ||
92 | 88 | conn = sqlite3.connect(rss_db_dir) | ||
93 | 89 | c = conn.cursor() | ||
94 | 90 | sql = ('select entries_no from ' + RssArchiver.table_name + | ||
95 | 91 | ' where list_name = \'' + mlist.fqdn_listname + '\'') | ||
96 | 92 | is_enabled = c.execute(sql).fetchone() | ||
97 | 93 | if is_enabled == None: | ||
98 | 94 | is_enabled = [100] | ||
99 | 95 | conn.commit() | ||
100 | 96 | c.close() | ||
101 | 97 | conn.close() | ||
102 | 98 | return is_enabled[0] | ||
103 | 99 | |||
104 | 100 | @staticmethod | ||
105 | 101 | def is_feed_enabled(mlist): | ||
106 | 102 | rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db') | ||
107 | 103 | RssArchiver.init_database() | ||
108 | 104 | conn = sqlite3.connect(rss_db_dir) | ||
109 | 105 | c = conn.cursor() | ||
110 | 106 | sql = ('select is_switched_on from ' + RssArchiver.table_name + | ||
111 | 107 | ' where list_name = \'' + mlist.fqdn_listname + '\'') | ||
112 | 108 | is_enabled = c.execute(sql).fetchone() | ||
113 | 109 | if is_enabled == None: | ||
114 | 110 | is_enabled = [1] | ||
115 | 111 | conn.commit() | ||
116 | 112 | c.close() | ||
117 | 113 | conn.close() | ||
118 | 114 | return is_enabled[0] | ||
119 | 115 | |||
120 | 116 | @staticmethod | ||
121 | 117 | def init_database(): | ||
122 | 118 | rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db') | ||
123 | 119 | conn = sqlite3.connect(rss_db_dir) | ||
124 | 120 | c = conn.cursor() | ||
125 | 121 | sql = ('create table if not exists ' + RssArchiver.table_name + | ||
126 | 122 | ' (list_name text unique, is_switched_on integer, entries_no integer)') | ||
127 | 123 | c.execute(sql) | ||
128 | 124 | conn.commit() | ||
129 | 125 | lists = [] | ||
130 | 126 | list_manager = getUtility(IListManager) | ||
131 | 127 | for fqdn_name in sorted(list_manager.names): | ||
132 | 128 | mlist = list_manager.get(fqdn_name) | ||
133 | 129 | lists.append(mlist) | ||
134 | 130 | for list in lists: | ||
135 | 131 | log.error(list.fqdn_listname) | ||
136 | 132 | sql = ('insert or ignore into ' + RssArchiver.table_name + | ||
137 | 133 | ' values (\'' + list.fqdn_listname + '\', 1, 30)') | ||
138 | 134 | c.execute(sql) | ||
139 | 135 | conn.commit() | ||
140 | 136 | c.close() | ||
141 | 137 | conn.close() | ||
142 | 138 | |||
143 | 139 | @staticmethod | ||
144 | 140 | def format_message(msg, msg_as_string): | ||
145 | 141 | string_msg = '' | ||
146 | 142 | string_msg += 'Date: ' + msg['date'] + '\n' | ||
147 | 143 | string_msg += 'From: ' + msg['from'] + '\n' | ||
148 | 144 | string_msg += 'To: ' + msg['to'] + '\n\n' + msg_as_string | ||
149 | 145 | return string_msg.replace('\n', '<br>').replace(' ', ' ') | ||
150 | 146 | |||
151 | 147 | @staticmethod | ||
152 | 148 | def extract_content(msg): | ||
153 | 149 | if msg.is_multipart(): | ||
154 | 150 | for part in msg.walk(): | ||
155 | 151 | if part.get_content_type() == 'text/plain': | ||
156 | 152 | msg_as_string = part.get_payload + msg_as_string | ||
157 | 153 | else: | ||
158 | 154 | msg_as_string = msg.get_payload() | ||
159 | 155 | return RssArchiver.format_message(msg, msg_as_string) | ||
160 | 156 | |||
161 | 157 | @staticmethod | ||
162 | 158 | def generate_rss_feed(mlist): | ||
163 | 159 | archive_dir = os.path.join(config.ARCHIVE_DIR, 'rssarchive') | ||
164 | 160 | RssArchiver.create_if_none(archive_dir) | ||
165 | 161 | list_dir = os.path.join(archive_dir, mlist.fqdn_listname) | ||
166 | 162 | mailbox = Maildir(list_dir, create=True, factory=None) | ||
167 | 163 | messages = mailbox.values() | ||
168 | 164 | for mes in messages: | ||
169 | 165 | mes['parsedate'] = parsedate(mes['date']) | ||
170 | 166 | messages.sort(key=itemgetter('parsedate'), reverse=True) | ||
171 | 167 | length_limit = RssArchiver.get_length_limit(mlist) | ||
172 | 168 | messages = messages[0: length_limit] | ||
173 | 169 | rss = PyRSS2Gen.RSS2( | ||
174 | 170 | title = mlist.fqdn_listname, | ||
175 | 171 | link = "", | ||
176 | 172 | description = "The latest messages from: " + mlist.fqdn_listname, | ||
177 | 173 | lastBuildDate = datetime.now(), | ||
178 | 174 | items = [ | ||
179 | 175 | PyRSS2Gen.RSSItem( | ||
180 | 176 | title = mes['subject'], | ||
181 | 177 | description = RssArchiver.extract_content(mes), | ||
182 | 178 | pubDate = mes['date']) | ||
183 | 179 | for mes in messages | ||
184 | 180 | ]) | ||
185 | 181 | dirPath = os.path.abspath("feeds") | ||
186 | 182 | if not os.path.exists(dirPath): | ||
187 | 183 | os.makedirs(dirPath) | ||
188 | 184 | fileName = mlist.fqdn_listname + '.xml' | ||
189 | 185 | filePath = os.path.abspath(os.path.join(dirPath, fileName)) | ||
190 | 186 | with open(filePath, "w") as f: | ||
191 | 187 | rss.write_xml(f) | ||
192 | 188 | |||
193 | 189 | @staticmethod | ||
194 | 190 | def list_url(mlist): | ||
195 | 191 | return None | ||
196 | 192 | |||
197 | 193 | @staticmethod | ||
198 | 194 | def permalink(mlist, msg): | ||
199 | 195 | return None | ||
200 | 196 | |||
201 | 197 | @staticmethod | ||
202 | 198 | def create_if_none(archive_dir): | ||
203 | 199 | try: | ||
204 | 200 | os.makedirs(archive_dir, 0775) | ||
205 | 201 | except OSError as error: | ||
206 | 202 | if error.errno != errno.EEXIST: | ||
207 | 203 | raise | ||
208 | 204 | |||
209 | 205 | @staticmethod | ||
210 | 206 | def archive_message(mlist, message): | ||
211 | 207 | archive_dir = os.path.join(config.ARCHIVE_DIR, 'rssarchive') | ||
212 | 208 | RssArchiver.create_if_none(archive_dir) | ||
213 | 209 | list_dir = os.path.join(archive_dir, mlist.fqdn_listname) | ||
214 | 210 | mailbox = Maildir(list_dir, create=True, factory=None) | ||
215 | 211 | lock_file = os.path.join( | ||
216 | 212 | config.LOCK_DIR, '{0}-maildir.lock'.format(mlist.fqdn_listname)) | ||
217 | 213 | lock = Lock(lock_file) | ||
218 | 214 | try: | ||
219 | 215 | lock.lock(timeout=timedelta(seconds=1)) | ||
220 | 216 | mailbox.add(message) | ||
221 | 217 | if RssArchiver.is_feed_enabled(mlist): | ||
222 | 218 | RssArchiver.generate_rss_feed(mlist); | ||
223 | 219 | except TimeOutError: | ||
224 | 220 | log.error('Unable to acquire rss archiver lock for {0}, ' | ||
225 | 221 | 'discarding: {1}'.format( | ||
226 | 222 | mlist.fqdn_listname, | ||
227 | 223 | message.get('message-id', 'n/a'))) | ||
228 | 224 | finally: | ||
229 | 225 | lock.unlock(unconditionally=True) | ||
230 | 0 | 226 | ||
231 | === added file 'src/mailman/archiving/tests/test_rssarchive.py' | |||
232 | --- src/mailman/archiving/tests/test_rssarchive.py 1970-01-01 00:00:00 +0000 | |||
233 | +++ src/mailman/archiving/tests/test_rssarchive.py 2013-09-01 16:37:50 +0000 | |||
234 | @@ -0,0 +1,203 @@ | |||
235 | 1 | # Copyright (C) 2012-2013 by the Free Software Foundation, Inc. | ||
236 | 2 | # | ||
237 | 3 | # This file is part of GNU Mailman. | ||
238 | 4 | # | ||
239 | 5 | # GNU Mailman is free software: you can redistribute it and/or modify it under | ||
240 | 6 | # the terms of the GNU General Public License as published by the Free | ||
241 | 7 | # Software Foundation, either version 3 of the License, or (at your option) | ||
242 | 8 | # any later version. | ||
243 | 9 | # | ||
244 | 10 | # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT | ||
245 | 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
246 | 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
247 | 13 | # more details. | ||
248 | 14 | # | ||
249 | 15 | # You should have received a copy of the GNU General Public License along with | ||
250 | 16 | # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. | ||
251 | 17 | |||
252 | 18 | """Test the rssarchiver.""" | ||
253 | 19 | |||
254 | 20 | from __future__ import absolute_import, print_function, unicode_literals | ||
255 | 21 | |||
256 | 22 | __metaclass__ = type | ||
257 | 23 | __all__ = [ | ||
258 | 24 | 'TestRssArchiver', | ||
259 | 25 | ] | ||
260 | 26 | |||
261 | 27 | |||
262 | 28 | import os | ||
263 | 29 | import shutil | ||
264 | 30 | import tempfile | ||
265 | 31 | import unittest | ||
266 | 32 | import threading | ||
267 | 33 | |||
268 | 34 | from xml.dom.minidom import parse, parseString | ||
269 | 35 | from email import message_from_file | ||
270 | 36 | from flufl.lock import Lock | ||
271 | 37 | |||
272 | 38 | from mailman.app.lifecycle import create_list | ||
273 | 39 | from mailman.archiving.rssarchive import RssArchiver | ||
274 | 40 | from mailman.config import config | ||
275 | 41 | from mailman.database.transaction import transaction | ||
276 | 42 | from mailman.testing.helpers import LogFileMark | ||
277 | 43 | from mailman.testing.helpers import ( | ||
278 | 44 | specialized_message_from_string as mfs) | ||
279 | 45 | from mailman.testing.layers import ConfigLayer | ||
280 | 46 | from mailman.utilities.email import add_message_hash | ||
281 | 47 | |||
282 | 48 | |||
283 | 49 | class TestRssArchiver(unittest.TestCase): | ||
284 | 50 | """Test the rss archiver.""" | ||
285 | 51 | |||
286 | 52 | layer = ConfigLayer | ||
287 | 53 | |||
288 | 54 | def setUp(self): | ||
289 | 55 | # Create a fake mailing list and message object | ||
290 | 56 | self._msg = mfs("""\ | ||
291 | 57 | To: test@example.com | ||
292 | 58 | From: anne@example.com | ||
293 | 59 | Date: 16-05-2012 | ||
294 | 60 | Subject: Testing the test list | ||
295 | 61 | Message-ID: <ant> | ||
296 | 62 | X-Message-ID-Hash: MS6QLWERIJLGCRF44J7USBFDELMNT2BW | ||
297 | 63 | |||
298 | 64 | Tests are better than no tests | ||
299 | 65 | but the water deserves to be swum. | ||
300 | 66 | """) | ||
301 | 67 | with transaction(): | ||
302 | 68 | self._mlist = create_list('test@example.com') | ||
303 | 69 | # Set up a temporary directory for the prototype archiver so that it's | ||
304 | 70 | # easier to clean up. | ||
305 | 71 | self._tempdir = tempfile.mkdtemp() | ||
306 | 72 | config.push('rssarchive', """ | ||
307 | 73 | [paths.testing] | ||
308 | 74 | archive_dir: {0} | ||
309 | 75 | """.format(self._tempdir)) | ||
310 | 76 | # Capture the structure of a maildir. | ||
311 | 77 | self._expected_dir_structure = set( | ||
312 | 78 | (os.path.join(config.ARCHIVE_DIR, path) for path in ( | ||
313 | 79 | 'rssarchive', | ||
314 | 80 | os.path.join('rssarchive', self._mlist.fqdn_listname), | ||
315 | 81 | os.path.join('rssarchive', self._mlist.fqdn_listname, 'cur'), | ||
316 | 82 | os.path.join('rssarchive', self._mlist.fqdn_listname, 'new'), | ||
317 | 83 | os.path.join('rssarchive', self._mlist.fqdn_listname, 'tmp'), | ||
318 | 84 | ))) | ||
319 | 85 | self._expected_dir_structure.add(config.ARCHIVE_DIR) | ||
320 | 86 | |||
321 | 87 | def tearDown(self): | ||
322 | 88 | shutil.rmtree(self._tempdir) | ||
323 | 89 | config.pop('rssarchive') | ||
324 | 90 | |||
325 | 91 | def _find(self, path): | ||
326 | 92 | all_filenames = set() | ||
327 | 93 | for dirpath, dirnames, filenames in os.walk(path): | ||
328 | 94 | if not isinstance(dirpath, unicode): | ||
329 | 95 | dirpath = unicode(dirpath) | ||
330 | 96 | all_filenames.add(dirpath) | ||
331 | 97 | for filename in filenames: | ||
332 | 98 | new_filename = filename | ||
333 | 99 | if not isinstance(filename, unicode): | ||
334 | 100 | new_filename = unicode(filename) | ||
335 | 101 | all_filenames.add(os.path.join(dirpath, new_filename)) | ||
336 | 102 | return all_filenames | ||
337 | 103 | |||
338 | 104 | def test_rssarchiver_xml_file(self): | ||
339 | 105 | RssArchiver.archive_message(self._mlist, self._msg) | ||
340 | 106 | xml_path = os.path.join('/root/mailman/parts/test/feeds/', (self._mlist.fqdn_listname + '.xml')) | ||
341 | 107 | |||
342 | 108 | datasource = open(xml_path) | ||
343 | 109 | dom = parse(datasource) | ||
344 | 110 | rssNode = dom.childNodes[0] | ||
345 | 111 | channelNode = rssNode.childNodes[0] | ||
346 | 112 | |||
347 | 113 | self.assertEqual(len(channelNode.childNodes), 7) | ||
348 | 114 | |||
349 | 115 | itemNode = channelNode.childNodes[6] | ||
350 | 116 | titleNode = itemNode.childNodes[0] | ||
351 | 117 | self.assertEqual(titleNode.nodeName, 'title') | ||
352 | 118 | self.assertEqual(titleNode.firstChild.nodeValue, 'Testing the test list') | ||
353 | 119 | |||
354 | 120 | def test_set_length_limit(self): | ||
355 | 121 | RssArchiver.set_length_limit(self._mlist, 2) | ||
356 | 122 | self.assertEqual(RssArchiver.get_length_limit(self._mlist), 2) | ||
357 | 123 | |||
358 | 124 | def test_set_feed_enabled(self): | ||
359 | 125 | RssArchiver.set_feed_enabled(self._mlist, 1) | ||
360 | 126 | self.assertEqual(RssArchiver.is_feed_enabled(self._mlist), 1) | ||
361 | 127 | RssArchiver.set_feed_enabled(self._mlist, 0) | ||
362 | 128 | self.assertEqual(RssArchiver.is_feed_enabled(self._mlist), 0) | ||
363 | 129 | |||
364 | 130 | def test_archive_maildir_created(self): | ||
365 | 131 | # Archiving a message to the rssarchiver should create the | ||
366 | 132 | # expected directory structure. | ||
367 | 133 | RssArchiver.archive_message(self._mlist, self._msg) | ||
368 | 134 | all_filenames = self._find(config.ARCHIVE_DIR) | ||
369 | 135 | # Check that the directory structure has been created and we have one | ||
370 | 136 | # more file (the archived message) than expected directories. | ||
371 | 137 | archived_messages = [x for x in (all_filenames - self._expected_dir_structure) if 'rss.db' not in x] | ||
372 | 138 | self.assertEqual(len(archived_messages), 1) | ||
373 | 139 | self.assertTrue( | ||
374 | 140 | archived_messages.pop().startswith( | ||
375 | 141 | os.path.join(config.ARCHIVE_DIR, 'rssarchive', | ||
376 | 142 | self._mlist.fqdn_listname, 'new'))) | ||
377 | 143 | |||
378 | 144 | def test_archive_maildir_existence_does_not_raise(self): | ||
379 | 145 | # Archiving a second message does not cause an EEXIST to be raised | ||
380 | 146 | # when a second message is archived. | ||
381 | 147 | new_dir = None | ||
382 | 148 | RssArchiver.archive_message(self._mlist, self._msg) | ||
383 | 149 | for directory in ('cur', 'new', 'tmp'): | ||
384 | 150 | path = os.path.join(config.ARCHIVE_DIR, 'rssarchive', | ||
385 | 151 | self._mlist.fqdn_listname, directory) | ||
386 | 152 | if directory == 'new': | ||
387 | 153 | new_dir = path | ||
388 | 154 | self.assertTrue(os.path.isdir(path)) | ||
389 | 155 | # There should be one message in the 'new' directory. | ||
390 | 156 | self.assertEqual(len(os.listdir(new_dir)), 1) | ||
391 | 157 | # Archive a second message. If an exception occurs, let it fail the | ||
392 | 158 | # test. Afterward, two messages should be in the 'new' directory. | ||
393 | 159 | del self._msg['message-id'] | ||
394 | 160 | del self._msg['x-message-id-hash'] | ||
395 | 161 | self._msg['Message-ID'] = '<bee>' | ||
396 | 162 | add_message_hash(self._msg) | ||
397 | 163 | RssArchiver.archive_message(self._mlist, self._msg) | ||
398 | 164 | self.assertEqual(len(os.listdir(new_dir)), 2) | ||
399 | 165 | |||
400 | 166 | def test_archive_lock_used(self): | ||
401 | 167 | # Test that locking the maildir when adding works as a failure here | ||
402 | 168 | # could mean we lose mail. | ||
403 | 169 | lock_file = os.path.join( | ||
404 | 170 | config.LOCK_DIR, '{0}-maildir.lock'.format( | ||
405 | 171 | self._mlist.fqdn_listname)) | ||
406 | 172 | with Lock(lock_file): | ||
407 | 173 | # Acquire the archiver lock, then make sure the archiver logs the | ||
408 | 174 | # fact that it could not acquire the lock. | ||
409 | 175 | archive_thread = threading.Thread( | ||
410 | 176 | target=RssArchiver.archive_message, | ||
411 | 177 | args=(self._mlist, self._msg)) | ||
412 | 178 | mark = LogFileMark('mailman.error') | ||
413 | 179 | archive_thread.run() | ||
414 | 180 | # Test that the archiver output the correct error. | ||
415 | 181 | line = mark.readline() | ||
416 | 182 | # XXX 2012-03-15 BAW: we really should remove timestamp prefixes | ||
417 | 183 | # from the loggers when under test. | ||
418 | 184 | self.assertTrue(line.endswith( | ||
419 | 185 | 'Unable to acquire rss archiver lock for {0}, ' | ||
420 | 186 | 'discarding: {1}\n'.format( | ||
421 | 187 | self._mlist.fqdn_listname, | ||
422 | 188 | self._msg.get('message-id')))) | ||
423 | 189 | # Check that the message didn't get archived. | ||
424 | 190 | created_files = self._find(config.ARCHIVE_DIR) | ||
425 | 191 | self.assertEqual(self._expected_dir_structure, created_files) | ||
426 | 192 | |||
427 | 193 | def test_rssarchiver_good_path(self): | ||
428 | 194 | # Verify the good path; the message gets archived. | ||
429 | 195 | RssArchiver.archive_message(self._mlist, self._msg) | ||
430 | 196 | new_path = os.path.join( | ||
431 | 197 | config.ARCHIVE_DIR, 'rssarchive', self._mlist.fqdn_listname, 'new') | ||
432 | 198 | archived_messages = list(os.listdir(new_path)) | ||
433 | 199 | self.assertEqual(len(archived_messages), 1) | ||
434 | 200 | # Check that the email has been added. | ||
435 | 201 | with open(os.path.join(new_path, archived_messages[0])) as fp: | ||
436 | 202 | archived_message = message_from_file(fp) | ||
437 | 203 | self.assertEqual(self._msg.as_string(), archived_message.as_string()) | ||
438 | 0 | 204 | ||
439 | === modified file 'src/mailman/config/mailman.cfg' | |||
440 | --- src/mailman/config/mailman.cfg 2013-01-16 23:54:32 +0000 | |||
441 | +++ src/mailman/config/mailman.cfg 2013-09-01 16:37:50 +0000 | |||
442 | @@ -91,3 +91,7 @@ | |||
443 | 91 | class: mailman.runners.digest.DigestRunner | 91 | class: mailman.runners.digest.DigestRunner |
444 | 92 | 92 | ||
445 | 93 | [style.default] | 93 | [style.default] |
446 | 94 | |||
447 | 95 | [archiver.rssarchive] | ||
448 | 96 | class: mailman.archiving.rssarchive.RssArchiver | ||
449 | 97 | enable: yes | ||
450 | 94 | 98 | ||
451 | === modified file 'src/mailman/rest/root.py' | |||
452 | --- src/mailman/rest/root.py 2013-01-01 14:05:42 +0000 | |||
453 | +++ src/mailman/rest/root.py 2013-09-01 16:37:50 +0000 | |||
454 | @@ -42,6 +42,7 @@ | |||
455 | 42 | from mailman.rest.preferences import ReadOnlyPreferences | 42 | from mailman.rest.preferences import ReadOnlyPreferences |
456 | 43 | from mailman.rest.templates import TemplateFinder | 43 | from mailman.rest.templates import TemplateFinder |
457 | 44 | from mailman.rest.users import AUser, AllUsers | 44 | from mailman.rest.users import AUser, AllUsers |
458 | 45 | from mailman.rest.rss import RssFeed | ||
459 | 45 | 46 | ||
460 | 46 | 47 | ||
461 | 47 | 48 | ||
462 | 48 | 49 | ||
463 | @@ -136,6 +137,14 @@ | |||
464 | 136 | return AList(list_identifier), segments | 137 | return AList(list_identifier), segments |
465 | 137 | 138 | ||
466 | 138 | @resource.child() | 139 | @resource.child() |
467 | 140 | def feeds(self, request, segments): | ||
468 | 141 | """/<api>/feeds/<list>/ | ||
469 | 142 | """ | ||
470 | 143 | #return http.ok([], etag({u'segments': segments[0]})) | ||
471 | 144 | list_identifier = segments.pop(0) | ||
472 | 145 | return RssFeed(list_identifier), segments | ||
473 | 146 | |||
474 | 147 | @resource.child() | ||
475 | 139 | def members(self, request, segments): | 148 | def members(self, request, segments): |
476 | 140 | """/<api>/members""" | 149 | """/<api>/members""" |
477 | 141 | if len(segments) == 0: | 150 | if len(segments) == 0: |
478 | 142 | 151 | ||
479 | === added file 'src/mailman/rest/rss.py' | |||
480 | --- src/mailman/rest/rss.py 1970-01-01 00:00:00 +0000 | |||
481 | +++ src/mailman/rest/rss.py 2013-09-01 16:37:50 +0000 | |||
482 | @@ -0,0 +1,72 @@ | |||
483 | 1 | # Copyright (C) 2010-2013 by the Free Software Foundation, Inc. | ||
484 | 2 | # | ||
485 | 3 | # This file is part of GNU Mailman. | ||
486 | 4 | # | ||
487 | 5 | # GNU Mailman is free software: you can redistribute it and/or modify it under | ||
488 | 6 | # the terms of the GNU General Public License as published by the Free | ||
489 | 7 | # Software Foundation, either version 3 of the License, or (at your option) | ||
490 | 8 | # any later version. | ||
491 | 9 | # | ||
492 | 10 | # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT | ||
493 | 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
494 | 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
495 | 13 | # more details. | ||
496 | 14 | # | ||
497 | 15 | # You should have received a copy of the GNU General Public License along with | ||
498 | 16 | # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. | ||
499 | 17 | |||
500 | 18 | """REST for members.""" | ||
501 | 19 | |||
502 | 20 | from __future__ import absolute_import, unicode_literals | ||
503 | 21 | |||
504 | 22 | __metaclass__ = type | ||
505 | 23 | __all__ = [ | ||
506 | 24 | 'RssAdmin', | ||
507 | 25 | ] | ||
508 | 26 | |||
509 | 27 | |||
510 | 28 | from uuid import UUID | ||
511 | 29 | from operator import attrgetter | ||
512 | 30 | from restish import http, resource | ||
513 | 31 | from zope.component import getUtility | ||
514 | 32 | |||
515 | 33 | from mailman.app.membership import delete_member | ||
516 | 34 | from mailman.interfaces.address import InvalidEmailAddressError | ||
517 | 35 | from mailman.interfaces.listmanager import IListManager, NoSuchListError | ||
518 | 36 | from mailman.interfaces.member import ( | ||
519 | 37 | AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError, | ||
520 | 38 | NotAMemberError) | ||
521 | 39 | from mailman.interfaces.subscriptions import ISubscriptionService | ||
522 | 40 | from mailman.interfaces.user import UnverifiedAddressError | ||
523 | 41 | from mailman.interfaces.usermanager import IUserManager | ||
524 | 42 | from mailman.rest.helpers import ( | ||
525 | 43 | CollectionMixin, PATCH, etag, no_content, paginate, path_to) | ||
526 | 44 | from mailman.rest.preferences import Preferences, ReadOnlyPreferences | ||
527 | 45 | from mailman.rest.validator import ( | ||
528 | 46 | Validator, enum_validator, subscriber_validator) | ||
529 | 47 | from mailman.archiving.rssarchive import RssArchiver | ||
530 | 48 | |||
531 | 49 | class RssFeed(resource.Resource): | ||
532 | 50 | def __init__(self, list_identifier): | ||
533 | 51 | # list-id is preferred, but for backward compatibility, fqdn_listname | ||
534 | 52 | # is also accepted. If the string contains '@', treat it as the | ||
535 | 53 | # latter. | ||
536 | 54 | manager = getUtility(IListManager) | ||
537 | 55 | if '@' in list_identifier: | ||
538 | 56 | self._mlist = manager.get(list_identifier) | ||
539 | 57 | else: | ||
540 | 58 | self._mlist = manager.get_by_list_id(list_identifier) | ||
541 | 59 | |||
542 | 60 | @resource.GET() | ||
543 | 61 | def getData(self, request): | ||
544 | 62 | is_feed_enabled = RssArchiver.is_feed_enabled(self._mlist) | ||
545 | 63 | size = RssArchiver.get_length_limit(self._mlist) | ||
546 | 64 | return http.ok([], etag({u'enabled': is_feed_enabled, u'sizeLimit': size})) | ||
547 | 65 | |||
548 | 66 | @resource.POST() | ||
549 | 67 | def setData(self, request): | ||
550 | 68 | length_limit = request.POST.get('size_limit') | ||
551 | 69 | is_enabled = request.POST.get('is_enabled') | ||
552 | 70 | RssArchiver.set_length_limit(self._mlist, length_limit) | ||
553 | 71 | RssArchiver.set_feed_enabled(self._mlist, is_enabled) | ||
554 | 72 | return no_content() |