Merge lp:~abompard/mailman/import21 into lp:mailman

Proposed by Aurélien Bompard
Status: Merged
Merge reported by: Barry Warsaw
Merged at revision: not available
Proposed branch: lp:~abompard/mailman/import21
Merge into: lp:mailman
Diff against target: 1337 lines (+1121/-19)
8 files modified
src/mailman/app/templates.py (+1/-1)
src/mailman/app/tests/test_templates.py (+11/-0)
src/mailman/commands/cli_import.py (+6/-2)
src/mailman/handlers/decorate.py (+7/-2)
src/mailman/model/listmanager.py (+2/-0)
src/mailman/model/tests/test_listmanager.py (+15/-0)
src/mailman/utilities/importer.py (+331/-7)
src/mailman/utilities/tests/test_import.py (+748/-7)
To merge this branch: bzr merge lp:~abompard/mailman/import21
Reviewer Review Type Date Requested Status
Barry Warsaw Abstain
Review via email: mp+192146@code.launchpad.net

Description of the change

This branch contains my work on the import21 command.

Most list properties are imported, as well as their rosters.
There are a few exceptions that I don't know how to handle, such as umbrella_list & umbrella_member_suffix, or the fact that header_filter_rules can't be directly converted to header_matches because the former used to match the whole header while the latter only matches the header value, not the header name itself, so I would need to "split" the regexp, which seems very brittle.

There are currently 67 unit tests (which are great for conversions), and it's been tested on about 300 mailing-lists (the Fedora project's lists).

To post a comment you must log in.
Revision history for this message
Aurélien Bompard (abompard) wrote :

Oh by the way, I'm not very at ease with Bazaar yet, so I may have made a mistake with the merging, please make sure I did not do something funny there... Sorry.

Revision history for this message
Aurélien Bompard (abompard) wrote :

Another thing, it contains a fix on mailman.app.templates (and the associated test case) to expect templates to be UTF-8 encoded, as discussed on the mailing-list.

Revision history for this message
Aurélien Bompard (abompard) wrote :

Hey Barry! If there's anything I can do help you merging this branch, please feel free to ask.

lp:~abompard/mailman/import21 updated
7235. By Aurélien Bompard

Merge from the main branch

Revision history for this message
Kẏra (thekyriarchy) wrote :

Barry, is there a reason this hasn't been reviewed and approved/rejected yet?

Revision history for this message
Barry Warsaw (barry) wrote :

Just to let you know, I am working my way through a review/merge of this branch.

review: Abstain

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/mailman/app/templates.py'
--- src/mailman/app/templates.py 2014-01-01 14:59:42 +0000
+++ src/mailman/app/templates.py 2014-01-27 11:01:58 +0000
@@ -103,4 +103,4 @@
103 def get(self, uri):103 def get(self, uri):
104 """See `ITemplateLoader`."""104 """See `ITemplateLoader`."""
105 with closing(urllib2.urlopen(uri)) as fp:105 with closing(urllib2.urlopen(uri)) as fp:
106 return fp.read()106 return unicode(fp.read(), "utf-8")
107107
=== modified file 'src/mailman/app/tests/test_templates.py'
--- src/mailman/app/tests/test_templates.py 2014-01-01 14:59:42 +0000
+++ src/mailman/app/tests/test_templates.py 2014-01-27 11:01:58 +0000
@@ -126,3 +126,14 @@
126 with self.assertRaises(urllib2.URLError) as cm:126 with self.assertRaises(urllib2.URLError) as cm:
127 self._loader.get('mailman:///missing@example.com/en/foo/demo.txt')127 self._loader.get('mailman:///missing@example.com/en/foo/demo.txt')
128 self.assertEqual(cm.exception.reason, 'No such file')128 self.assertEqual(cm.exception.reason, 'No such file')
129
130 def test_non_ascii(self):
131 # mailman://demo.txt with non-ascii content
132 test_text = b'\xe4\xb8\xad'
133 path = os.path.join(self.var_dir, 'templates', 'site', 'it')
134 os.makedirs(path)
135 with open(os.path.join(path, 'demo.txt'), 'w') as fp:
136 print(test_text, end='', file=fp)
137 content = self._loader.get('mailman:///it/demo.txt')
138 self.assertTrue(isinstance(content, unicode))
139 self.assertEqual(content, test_text.decode("utf-8"))
129140
=== modified file 'src/mailman/commands/cli_import.py'
--- src/mailman/commands/cli_import.py 2014-01-01 14:59:42 +0000
+++ src/mailman/commands/cli_import.py 2014-01-27 11:01:58 +0000
@@ -35,7 +35,7 @@
35from mailman.database.transaction import transactional35from mailman.database.transaction import transactional
36from mailman.interfaces.command import ICLISubCommand36from mailman.interfaces.command import ICLISubCommand
37from mailman.interfaces.listmanager import IListManager37from mailman.interfaces.listmanager import IListManager
38from mailman.utilities.importer import import_config_pck38from mailman.utilities.importer import import_config_pck, Import21Error
3939
4040
4141
4242
@@ -93,4 +93,8 @@
93 print(_('Ignoring non-dictionary: {0!r}').format(93 print(_('Ignoring non-dictionary: {0!r}').format(
94 config_dict), file=sys.stderr)94 config_dict), file=sys.stderr)
95 continue95 continue
96 import_config_pck(mlist, config_dict)96 try:
97 import_config_pck(mlist, config_dict)
98 except Import21Error, e:
99 print(e, file=sys.stderr)
100 sys.exit(1)
97101
=== modified file 'src/mailman/handlers/decorate.py'
--- src/mailman/handlers/decorate.py 2014-01-01 14:59:42 +0000
+++ src/mailman/handlers/decorate.py 2014-01-27 11:01:58 +0000
@@ -201,8 +201,8 @@
201201
202202
203203
204def decorate(mlist, uri, extradict=None):204def decorate(mlist, uri, extradict=None):
205 """Expand the decoration template."""205 """Expand the decoration template from its URI."""
206 if uri is None:206 if uri is None or uri == '':
207 return ''207 return ''
208 # Get the decorator template.208 # Get the decorator template.
209 loader = getUtility(ITemplateLoader)209 loader = getUtility(ITemplateLoader)
@@ -211,6 +211,11 @@
211 language=mlist.preferred_language.code,211 language=mlist.preferred_language.code,
212 ))212 ))
213 template = loader.get(template_uri)213 template = loader.get(template_uri)
214 return decorate_template(mlist, template, extradict)
215
216
214217
218def decorate_template(mlist, template, extradict=None):
219 """Expand the decoration template."""
215 # Create a dictionary which includes the default set of interpolation220 # Create a dictionary which includes the default set of interpolation
216 # variables allowed in headers and footers. These will be augmented by221 # variables allowed in headers and footers. These will be augmented by
217 # any key/value pairs in the extradict.222 # any key/value pairs in the extradict.
218223
=== modified file 'src/mailman/model/listmanager.py'
--- src/mailman/model/listmanager.py 2014-01-01 14:59:42 +0000
+++ src/mailman/model/listmanager.py 2014-01-27 11:01:58 +0000
@@ -34,6 +34,7 @@
34 IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent,34 IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent,
35 ListDeletedEvent, ListDeletingEvent)35 ListDeletedEvent, ListDeletingEvent)
36from mailman.model.mailinglist import MailingList36from mailman.model.mailinglist import MailingList
37from mailman.model.mime import ContentFilter
37from mailman.utilities.datetime import now38from mailman.utilities.datetime import now
3839
3940
@@ -79,6 +80,7 @@
79 """See `IListManager`."""80 """See `IListManager`."""
80 fqdn_listname = mlist.fqdn_listname81 fqdn_listname = mlist.fqdn_listname
81 notify(ListDeletingEvent(mlist))82 notify(ListDeletingEvent(mlist))
83 store.find(ContentFilter, ContentFilter.mailing_list == mlist).remove()
82 store.remove(mlist)84 store.remove(mlist)
83 notify(ListDeletedEvent(fqdn_listname))85 notify(ListDeletedEvent(fqdn_listname))
8486
8587
=== modified file 'src/mailman/model/tests/test_listmanager.py'
--- src/mailman/model/tests/test_listmanager.py 2014-01-01 14:59:42 +0000
+++ src/mailman/model/tests/test_listmanager.py 2014-01-27 11:01:58 +0000
@@ -30,6 +30,7 @@
30import unittest30import unittest
3131
32from zope.component import getUtility32from zope.component import getUtility
33from storm.locals import Store
3334
34from mailman.app.lifecycle import create_list35from mailman.app.lifecycle import create_list
35from mailman.app.moderator import hold_message36from mailman.app.moderator import hold_message
@@ -40,6 +41,7 @@
40from mailman.interfaces.requests import IListRequests41from mailman.interfaces.requests import IListRequests
41from mailman.interfaces.subscriptions import ISubscriptionService42from mailman.interfaces.subscriptions import ISubscriptionService
42from mailman.interfaces.usermanager import IUserManager43from mailman.interfaces.usermanager import IUserManager
44from mailman.model.mime import ContentFilter
43from mailman.testing.helpers import (45from mailman.testing.helpers import (
44 event_subscribers, specialized_message_from_string)46 event_subscribers, specialized_message_from_string)
45from mailman.testing.layers import ConfigLayer47from mailman.testing.layers import ConfigLayer
@@ -129,6 +131,19 @@
129 saved_message = getUtility(IMessageStore).get_message_by_id('<argon>')131 saved_message = getUtility(IMessageStore).get_message_by_id('<argon>')
130 self.assertEqual(saved_message.as_string(), msg.as_string())132 self.assertEqual(saved_message.as_string(), msg.as_string())
131133
134 def test_content_filters_are_deleted_when_mailing_list_is_deleted(self):
135 # When a mailing list with content filters is deleted, the filters must
136 # be deleted fist or an IntegrityError will be raised
137 filter_names = ("filter_types", "pass_types",
138 "filter_extensions", "pass_extensions")
139 for fname in filter_names:
140 setattr(self._ant, fname, ["test-filter-1", "test-filter-2"])
141 getUtility(IListManager).delete(self._ant)
142 store = Store.of(self._ant)
143 filters = store.find(ContentFilter,
144 ContentFilter.mailing_list == self._ant)
145 self.assertEqual(filters.count(), 0)
146
132147
133148
134149
135class TestListCreation(unittest.TestCase):150class TestListCreation(unittest.TestCase):
136151
=== modified file 'src/mailman/utilities/importer.py'
--- src/mailman/utilities/importer.py 2014-01-01 14:59:42 +0000
+++ src/mailman/utilities/importer.py 2014-01-27 11:01:58 +0000
@@ -22,45 +22,183 @@
22__metaclass__ = type22__metaclass__ = type
23__all__ = [23__all__ = [
24 'import_config_pck',24 'import_config_pck',
25 'Import21Error',
25 ]26 ]
2627
2728
28import sys29import sys
29import datetime30import datetime
31import os
32from urllib2 import URLError
3033
31from mailman.interfaces.action import FilterAction34from mailman.config import config
32from mailman.interfaces.archiver import ArchivePolicy35from mailman.core.errors import MailmanError
36from mailman.interfaces.action import FilterAction, Action
33from mailman.interfaces.autorespond import ResponseAction37from mailman.interfaces.autorespond import ResponseAction
34from mailman.interfaces.digests import DigestFrequency38from mailman.interfaces.digests import DigestFrequency
35from mailman.interfaces.mailinglist import Personalization, ReplyToMunging39from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
36from mailman.interfaces.nntp import NewsgroupModeration40from mailman.interfaces.nntp import NewsgroupModeration
3741from mailman.interfaces.archiver import ArchivePolicy
42from mailman.interfaces.bans import IBanManager
43from mailman.interfaces.mailinglist import IAcceptableAliasSet
44from mailman.interfaces.bounce import UnrecognizedBounceDisposition
45from mailman.interfaces.usermanager import IUserManager
46from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole
47from mailman.interfaces.languages import ILanguageManager
48from mailman.handlers.decorate import decorate, decorate_template
49from mailman.utilities.i18n import search
50from zope.component import getUtility
51
52
53
3854
55class Import21Error(MailmanError):
56 pass
57
58
3959
60def str_to_unicode(value):
61 # Convert a string to unicode when the encoding is not declared
62 if isinstance(value, unicode):
63 return value
64 for encoding in ("ascii", "utf-8"):
65 try:
66 return unicode(value, encoding)
67 except UnicodeDecodeError:
68 continue
69 # we did our best, use replace
70 return unicode(value, 'ascii', 'replace')
4071
4172
4273
43def seconds_to_delta(value):74def seconds_to_delta(value):
44 return datetime.timedelta(seconds=value)75 return datetime.timedelta(seconds=value)
4576
77
4678
79def days_to_delta(value):
80 return datetime.timedelta(days=value)
81
82
4783
84def list_members_to_unicode(value):
85 return [ unicode(item) for item in value ]
86
87
4888
89def filter_action_mapping(value):
90 # The filter_action enum values have changed. In Mailman 2.1 the order was
91 # 'Discard', 'Reject', 'Forward to List Owner', 'Preserve'.
92 # In 3.0 it's 'hold', 'reject', 'discard', 'accept', 'defer', 'forward',
93 # 'preserve'
94 if value == 0:
95 return FilterAction.discard
96 elif value == 1:
97 return FilterAction.reject
98 elif value == 2:
99 return FilterAction.forward
100 elif value == 3:
101 return FilterAction.preserve
102 else:
103 raise ValueError("Unknown filter_action value: %s" % value)
104
105
49106
107def member_action_mapping(value):
108 # The mlist.default_member_action and mlist.default_nonmember_action enum
109 # values are different in Mailman 2.1, because they have been merged into a
110 # single enum in Mailman 3
111 # For default_member_action, which used to be called
112 # member_moderation_action, the values were:
113 # 0==Hold, 1=Reject, 2==Discard
114 if value == 0:
115 return Action.hold
116 elif value == 1:
117 return Action.reject
118 elif value == 2:
119 return Action.discard
120def nonmember_action_mapping(value):
121 # For default_nonmember_action, which used to be called
122 # generic_nonmember_action, the values were:
123 # 0==Accept, 1==Hold, 2==Reject, 3==Discard
124 if value == 0:
125 return Action.accept
126 elif value == 1:
127 return Action.hold
128 elif value == 2:
129 return Action.reject
130 elif value == 3:
131 return Action.discard
132
133
50134
135def unicode_to_string(value):
136 return str(value) if value is not None else None
137
138
51139
140def check_language_code(code):
141 if code is None:
142 return None
143 code = unicode(code)
144 if code not in getUtility(ILanguageManager):
145 msg = """Missing language: {0}
146You must add a section describing this language in your mailman.cfg file.
147This section should look like this:
148[language.{0}]
149# The English name for the language.
150description: CHANGE ME
151# And the default character set for the language.
152charset: utf-8
153# Whether the language is enabled or not.
154enabled: yes
155""".format(code)
156 raise Import21Error(msg)
157 return code
158
52159
53# Attributes in Mailman 2 which have a different type in Mailman 3.160# Attributes in Mailman 2 which have a different type in Mailman 3.
54TYPES = dict(161TYPES = dict(
55 autorespond_owner=ResponseAction,162 autorespond_owner=ResponseAction,
56 autorespond_postings=ResponseAction,163 autorespond_postings=ResponseAction,
57 autorespond_requests=ResponseAction,164 autorespond_requests=ResponseAction,
165 autoresponse_grace_period=days_to_delta,
58 bounce_info_stale_after=seconds_to_delta,166 bounce_info_stale_after=seconds_to_delta,
59 bounce_you_are_disabled_warnings_interval=seconds_to_delta,167 bounce_you_are_disabled_warnings_interval=seconds_to_delta,
60 digest_volume_frequency=DigestFrequency,168 digest_volume_frequency=DigestFrequency,
61 filter_action=FilterAction,169 filter_action=filter_action_mapping,
62 newsgroup_moderation=NewsgroupModeration,170 newsgroup_moderation=NewsgroupModeration,
63 personalize=Personalization,171 personalize=Personalization,
64 reply_goes_to_list=ReplyToMunging,172 reply_goes_to_list=ReplyToMunging,
173 filter_types=list_members_to_unicode,
174 pass_types=list_members_to_unicode,
175 filter_extensions=list_members_to_unicode,
176 pass_extensions=list_members_to_unicode,
177 forward_unrecognized_bounces_to=UnrecognizedBounceDisposition,
178 default_member_action=member_action_mapping,
179 default_nonmember_action=nonmember_action_mapping,
180 moderator_password=unicode_to_string,
181 preferred_language=check_language_code,
65 )182 )
66183
67184
68# Attribute names in Mailman 2 which are renamed in Mailman 3.185# Attribute names in Mailman 2 which are renamed in Mailman 3.
69NAME_MAPPINGS = dict(186NAME_MAPPINGS = dict(
70 host_name='mail_host',
71 include_list_post_header='allow_list_posts',187 include_list_post_header='allow_list_posts',
72 real_name='display_name',188 real_name='display_name',
189 last_post_time='last_post_at',
190 autoresponse_graceperiod='autoresponse_grace_period',
191 autorespond_admin='autorespond_owner',
192 autoresponse_admin_text='autoresponse_owner_text',
193 filter_mime_types='filter_types',
194 pass_mime_types='pass_types',
195 filter_filename_extensions='filter_extensions',
196 pass_filename_extensions='pass_extensions',
197 bounce_processing='process_bounces',
198 bounce_unrecognized_goes_to_list_owner='forward_unrecognized_bounces_to',
199 mod_password='moderator_password',
200 news_moderation='newsgroup_moderation',
201 news_prefix_subject_too='nntp_prefix_subject_too',
202 send_welcome_msg='send_welcome_message',
203 send_goodbye_msg='send_goodbye_message',
204 member_moderation_action='default_member_action',
205 generic_nonmember_action='default_nonmember_action',
206 )
207
208EXCLUDES = (
209 "members",
210 "digest_members",
73 )211 )
74212
75213
@@ -74,14 +212,19 @@
74 :type config_dict: dict212 :type config_dict: dict
75 """213 """
76 for key, value in config_dict.items():214 for key, value in config_dict.items():
215 # Some attributes must not be directly imported
216 if key in EXCLUDES:
217 continue
77 # Some attributes from Mailman 2 were renamed in Mailman 3.218 # Some attributes from Mailman 2 were renamed in Mailman 3.
78 key = NAME_MAPPINGS.get(key, key)219 key = NAME_MAPPINGS.get(key, key)
79 # Handle the simple case where the key is an attribute of the220 # Handle the simple case where the key is an attribute of the
80 # IMailingList and the types are the same (modulo 8-bit/unicode221 # IMailingList and the types are the same (modulo 8-bit/unicode
81 # strings).222 # strings).
82 if hasattr(mlist, key):223 # When attributes raise an exception, hasattr may think they don't
224 # exist (see python issue 9666). Add them here.
225 if hasattr(mlist, key) or key in ("preferred_language", ):
83 if isinstance(value, str):226 if isinstance(value, str):
84 value = unicode(value, 'ascii')227 value = str_to_unicode(value)
85 # Some types require conversion.228 # Some types require conversion.
86 converter = TYPES.get(key)229 converter = TYPES.get(key)
87 if converter is not None:230 if converter is not None:
@@ -103,3 +246,184 @@
103 mlist.archive_policy = ArchivePolicy.public246 mlist.archive_policy = ArchivePolicy.public
104 else:247 else:
105 mlist.archive_policy = ArchivePolicy.never248 mlist.archive_policy = ArchivePolicy.never
249 # Handle ban list
250 for addr in config_dict.get('ban_list', []):
251 IBanManager(mlist).ban(str_to_unicode(addr))
252 # Handle acceptable aliases
253 acceptable_aliases = config_dict.get('acceptable_aliases', '')
254 if isinstance(acceptable_aliases, basestring):
255 acceptable_aliases = acceptable_aliases.splitlines()
256 for addr in acceptable_aliases:
257 addr = addr.strip()
258 if not addr:
259 continue
260 addr = str_to_unicode(addr)
261 try:
262 IAcceptableAliasSet(mlist).add(addr)
263 except ValueError:
264 IAcceptableAliasSet(mlist).add("^" + addr)
265 # Handle conversion to URIs
266 convert_to_uri = {
267 "welcome_msg": "welcome_message_uri",
268 "goodbye_msg": "goodbye_message_uri",
269 "msg_header": "header_uri",
270 "msg_footer": "footer_uri",
271 "digest_header": "digest_header_uri",
272 "digest_footer": "digest_footer_uri",
273 }
274 convert_placeholders = { # only the most common ones
275 "%(real_name)s": "$display_name",
276 "%(real_name)s@%(host_name)s": "$fqdn_listname",
277 "%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s": "$listinfo_uri",
278 }
279 # Collect defaults
280 defaults = {}
281 for oldvar, newvar in convert_to_uri.iteritems():
282 default_value = getattr(mlist, newvar)
283 if not default_value:
284 continue
285 # Check if the value changed from the default
286 try:
287 default_text = decorate(mlist, default_value)
288 except (URLError, KeyError):
289 # Use case: importing the old a@ex.com into b@ex.com
290 # We can't check if it changed from the default
291 # -> don't import, we may do more harm than good and it's easy to
292 # change if needed
293 continue
294 defaults[newvar] = (default_value, default_text)
295 for oldvar, newvar in convert_to_uri.iteritems():
296 if oldvar not in config_dict:
297 continue
298 text = config_dict[oldvar]
299 text = unicode(text, "utf-8", "replace")
300 for oldph, newph in convert_placeholders.iteritems():
301 text = text.replace(oldph, newph)
302 default_value, default_text = defaults.get(newvar, (None, None))
303 if not text and not (default_value or default_text):
304 continue # both are empty, leave it
305 # Check if the value changed from the default
306 try:
307 expanded_text = decorate_template(mlist, text)
308 except KeyError:
309 # Use case: importing the old a@ex.com into b@ex.com
310 # We can't check if it changed from the default
311 # -> don't import, we may do more harm than good and it's easy to
312 # change if needed
313 continue
314 if expanded_text and default_text \
315 and expanded_text.strip() == default_text.strip():
316 continue # keep the default
317 # Write the custom value to the right file
318 base_uri = "mailman:///$listname/$language/"
319 if default_value:
320 filename = default_value.rpartition("/")[2]
321 else:
322 filename = "%s.txt" % newvar[:-4]
323 if not default_value or not default_value.startswith(base_uri):
324 setattr(mlist, newvar, base_uri + filename)
325 filepath = list(search(filename, mlist))[0]
326 try:
327 os.makedirs(os.path.dirname(filepath))
328 except OSError, e:
329 if e.errno != 17: # Already exists
330 raise
331 with open(filepath, "w") as template:
332 template.write(text.encode('utf-8'))
333 # Import rosters
334 members = set(config_dict.get("members", {}).keys()
335 + config_dict.get("digest_members", {}).keys())
336 import_roster(mlist, config_dict, members, MemberRole.member)
337 import_roster(mlist, config_dict, config_dict.get("owner", []),
338 MemberRole.owner)
339 import_roster(mlist, config_dict, config_dict.get("moderator", []),
340 MemberRole.moderator)
341
342
343
106344
345def import_roster(mlist, config_dict, members, role):
346 """
347 Import members lists from a config.pck configuration dictionary to a
348 mailing list.
349
350 :param mlist: The mailing list.
351 :type mlist: IMailingList
352 :param config_dict: The Mailman 2.1 configuration dictionary.
353 :type config_dict: dict
354 :param members: The members list to import
355 :type members: list
356 :param role: The MemberRole to import them as
357 :type role: MemberRole enum
358 """
359 usermanager = getUtility(IUserManager)
360 for email in members:
361 # for owners and members, the emails can have a mixed case, so
362 # lowercase them all
363 email = str_to_unicode(email).lower()
364 roster = mlist.get_roster(role)
365 if roster.get_member(email) is not None:
366 print("%s is already imported with role %s" % (email, role),
367 file=sys.stderr)
368 continue
369 address = usermanager.get_address(email)
370 user = usermanager.get_user(email)
371 if user is None:
372 user = usermanager.create_user()
373 if address is None:
374 merged_members = {}
375 merged_members.update(config_dict.get("members", {}))
376 merged_members.update(config_dict.get("digest_members", {}))
377 if merged_members.get(email, 0) != 0:
378 original_email = str_to_unicode(merged_members[email])
379 else:
380 original_email = email
381 address = usermanager.create_address(original_email)
382 address.verified_on = datetime.datetime.now()
383 user.link(address)
384 mlist.subscribe(address, role)
385 member = roster.get_member(email)
386 assert member is not None
387 prefs = config_dict.get("user_options", {}).get(email, 0)
388 if email in config_dict.get("members", {}):
389 member.preferences.delivery_mode = DeliveryMode.regular
390 elif email in config_dict.get("digest_members", {}):
391 if prefs & 8: # DisableMime
392 member.preferences.delivery_mode = DeliveryMode.plaintext_digests
393 else:
394 member.preferences.delivery_mode = DeliveryMode.mime_digests
395 else:
396 # probably not adding a member role here
397 pass
398 if email in config_dict.get("language", {}):
399 member.preferences.preferred_language = \
400 check_language_code(config_dict["language"][email])
401 # if the user already exists, display_name and password will be
402 # overwritten
403 if email in config_dict.get("usernames", {}):
404 address.display_name = \
405 str_to_unicode(config_dict["usernames"][email])
406 user.display_name = \
407 str_to_unicode(config_dict["usernames"][email])
408 if email in config_dict.get("passwords", {}):
409 user.password = config.password_context.encrypt(
410 config_dict["passwords"][email])
411 # delivery_status
412 oldds = config_dict.get("delivery_status", {}).get(email, (0, 0))[0]
413 if oldds == 0:
414 member.preferences.delivery_status = DeliveryStatus.enabled
415 elif oldds == 1:
416 member.preferences.delivery_status = DeliveryStatus.unknown
417 elif oldds == 2:
418 member.preferences.delivery_status = DeliveryStatus.by_user
419 elif oldds == 3:
420 member.preferences.delivery_status = DeliveryStatus.by_moderator
421 elif oldds == 4:
422 member.preferences.delivery_status = DeliveryStatus.by_bounces
423 # moderation
424 if prefs & 128:
425 member.moderation_action = Action.hold
426 # other preferences
427 member.preferences.acknowledge_posts = bool(prefs & 4) # AcknowledgePosts
428 member.preferences.hide_address = bool(prefs & 16) # ConcealSubscription
429 member.preferences.receive_own_postings = not bool(prefs & 2) # DontReceiveOwnPosts
430 member.preferences.receive_list_copy = not bool(prefs & 256) # DontReceiveDuplicates
107431
=== modified file 'src/mailman/utilities/tests/test_import.py'
--- src/mailman/utilities/tests/test_import.py 2014-01-01 14:59:42 +0000
+++ src/mailman/utilities/tests/test_import.py 2014-01-27 11:01:58 +0000
@@ -26,15 +26,41 @@
26 ]26 ]
2727
2828
29import os
29import cPickle30import cPickle
30import unittest31import unittest
32from datetime import timedelta, datetime
33from traceback import format_exc
3134
35from mailman.config import config
32from mailman.app.lifecycle import create_list, remove_list36from mailman.app.lifecycle import create_list, remove_list
37from mailman.testing.layers import ConfigLayer
38from mailman.utilities.importer import import_config_pck, Import21Error
33from mailman.interfaces.archiver import ArchivePolicy39from mailman.interfaces.archiver import ArchivePolicy
34from mailman.testing.layers import ConfigLayer40from mailman.interfaces.action import Action, FilterAction
35from mailman.utilities.importer import import_config_pck41from mailman.interfaces.address import ExistingAddressError
42from mailman.interfaces.bounce import UnrecognizedBounceDisposition
43from mailman.interfaces.bans import IBanManager
44from mailman.interfaces.mailinglist import IAcceptableAliasSet
45from mailman.interfaces.nntp import NewsgroupModeration
46from mailman.interfaces.autorespond import ResponseAction
47from mailman.interfaces.templates import ITemplateLoader
48from mailman.interfaces.usermanager import IUserManager
49from mailman.interfaces.member import DeliveryMode, DeliveryStatus
50from mailman.interfaces.languages import ILanguageManager
51from mailman.model.address import Address
52from mailman.handlers.decorate import decorate
53from mailman.utilities.string import expand
36from pkg_resources import resource_filename54from pkg_resources import resource_filename
3755from enum import Enum
56from zope.component import getUtility
57from storm.locals import Store
58
59
60
3861
62class DummyEnum(Enum):
63 # For testing purposes
64 val = 42
3965
4066
4167
42class TestBasicImport(unittest.TestCase):68class TestBasicImport(unittest.TestCase):
@@ -58,11 +84,12 @@
58 self._import()84 self._import()
59 self.assertEqual(self._mlist.display_name, 'Test')85 self.assertEqual(self._mlist.display_name, 'Test')
6086
61 def test_mail_host(self):87 def test_mail_host_invariant(self):
62 # The mlist.mail_host gets set.88 # The mlist.mail_host must not be updated when importing (it will
89 # change the list_id property, which is supposed to be read-only)
63 self.assertEqual(self._mlist.mail_host, 'example.com')90 self.assertEqual(self._mlist.mail_host, 'example.com')
64 self._import()91 self._import()
65 self.assertEqual(self._mlist.mail_host, 'heresy.example.org')92 self.assertEqual(self._mlist.mail_host, 'example.com')
6693
67 def test_rfc2369_headers(self):94 def test_rfc2369_headers(self):
68 self._mlist.allow_list_posts = False95 self._mlist.allow_list_posts = False
@@ -71,6 +98,204 @@
71 self.assertTrue(self._mlist.allow_list_posts)98 self.assertTrue(self._mlist.allow_list_posts)
72 self.assertTrue(self._mlist.include_rfc2369_headers)99 self.assertTrue(self._mlist.include_rfc2369_headers)
73100
101 def test_no_overwrite_rosters(self):
102 # The mlist.members and mlist.digest_members rosters must not be
103 # overwritten.
104 for rname in ("members", "digest_members"):
105 roster = getattr(self._mlist, rname)
106 self.assertFalse(isinstance(roster, dict))
107 self._import()
108 self.assertFalse(isinstance(roster, dict),
109 "The %s roster has been overwritten by the import" % rname)
110
111 def test_last_post_time(self):
112 # last_post_time -> last_post_at
113 self._pckdict["last_post_time"] = 1270420800.274485
114 self.assertEqual(self._mlist.last_post_at, None)
115 self._import()
116 # convert 1270420800.2744851 to datetime
117 expected = datetime(2010, 4, 4, 22, 40, 0, 274485)
118 self.assertEqual(self._mlist.last_post_at, expected)
119
120 def test_autoresponse_grace_period(self):
121 # autoresponse_graceperiod -> autoresponse_grace_period
122 # must be a timedelta, not an int
123 self._mlist.autoresponse_grace_period = timedelta(days=42)
124 self._import()
125 self.assertTrue(isinstance(
126 self._mlist.autoresponse_grace_period, timedelta))
127 self.assertEqual(self._mlist.autoresponse_grace_period,
128 timedelta(days=90))
129
130 def test_autoresponse_admin_to_owner(self):
131 # admin -> owner
132 self._mlist.autorespond_owner = DummyEnum.val
133 self._mlist.autoresponse_owner_text = 'DUMMY'
134 self._import()
135 self.assertEqual(self._mlist.autorespond_owner, ResponseAction.none)
136 self.assertEqual(self._mlist.autoresponse_owner_text, '')
137
138 #def test_administrative(self):
139 # # administrivia -> administrative
140 # self._mlist.administrative = None
141 # self._import()
142 # self.assertTrue(self._mlist.administrative)
143
144 def test_filter_pass_renames(self):
145 # mime_types -> types
146 # filename_extensions -> extensions
147 self._mlist.filter_types = ["dummy"]
148 self._mlist.pass_types = ["dummy"]
149 self._mlist.filter_extensions = ["dummy"]
150 self._mlist.pass_extensions = ["dummy"]
151 self._import()
152 self.assertEqual(list(self._mlist.filter_types), [])
153 self.assertEqual(list(self._mlist.filter_extensions),
154 ['exe', 'bat', 'cmd', 'com', 'pif',
155 'scr', 'vbs', 'cpl'])
156 self.assertEqual(list(self._mlist.pass_types),
157 ['multipart/mixed', 'multipart/alternative', 'text/plain'])
158 self.assertEqual(list(self._mlist.pass_extensions), [])
159
160 def test_process_bounces(self):
161 # bounce_processing -> process_bounces
162 self._mlist.process_bounces = None
163 self._import()
164 self.assertTrue(self._mlist.process_bounces)
165
166 def test_forward_unrecognized_bounces_to(self):
167 # bounce_unrecognized_goes_to_list_owner -> forward_unrecognized_bounces_to
168 self._mlist.forward_unrecognized_bounces_to = DummyEnum.val
169 self._import()
170 self.assertEqual(self._mlist.forward_unrecognized_bounces_to,
171 UnrecognizedBounceDisposition.administrators)
172
173 def test_moderator_password(self):
174 # mod_password -> moderator_password
175 self._mlist.moderator_password = str("TESTDATA")
176 self._import()
177 self.assertEqual(self._mlist.moderator_password, None)
178
179 def test_moderator_password_str(self):
180 # moderator_password must not be unicode
181 self._pckdict[b"mod_password"] = b'TESTVALUE'
182 self._import()
183 self.assertFalse(isinstance(self._mlist.moderator_password, unicode))
184 self.assertEqual(self._mlist.moderator_password, b'TESTVALUE')
185
186 def test_newsgroup_moderation(self):
187 # news_moderation -> newsgroup_moderation
188 # news_prefix_subject_too -> nntp_prefix_subject_too
189 self._mlist.newsgroup_moderation = DummyEnum.val
190 self._mlist.nntp_prefix_subject_too = None
191 self._import()
192 self.assertEqual(self._mlist.newsgroup_moderation,
193 NewsgroupModeration.none)
194 self.assertTrue(self._mlist.nntp_prefix_subject_too)
195
196 def test_msg_to_message(self):
197 # send_welcome_msg -> send_welcome_message
198 # send_goodbye_msg -> send_goodbye_message
199 self._mlist.send_welcome_message = None
200 self._mlist.send_goodbye_message = None
201 self._import()
202 self.assertTrue(self._mlist.send_welcome_message)
203 self.assertTrue(self._mlist.send_goodbye_message)
204
205 def test_ban_list(self):
206 banned = [
207 ("anne@example.com", "anne@example.com"),
208 ("^.*@example.com", "bob@example.com"),
209 ("non-ascii-\xe8@example.com", "non-ascii-\ufffd@example.com"),
210 ]
211 self._pckdict["ban_list"] = [ b[0].encode("iso-8859-1") for b in banned ]
212 try:
213 self._import()
214 except UnicodeDecodeError, e:
215 print(format_exc())
216 self.fail(e)
217 for _pattern, addr in banned:
218 self.assertTrue(IBanManager(self._mlist).is_banned(addr))
219
220 def test_acceptable_aliases(self):
221 # it used to be a plain-text field (values are newline-separated)
222 aliases = ["alias1@example.com",
223 "alias2@exemple.com",
224 "non-ascii-\xe8@example.com",
225 ]
226 self._pckdict[b"acceptable_aliases"] = \
227 ("\n".join(aliases)).encode("utf-8")
228 self._import()
229 alias_set = IAcceptableAliasSet(self._mlist)
230 self.assertEqual(sorted(alias_set.aliases), aliases)
231
232 def test_acceptable_aliases_invalid(self):
233 # values without an '@' sign used to be matched against the local part,
234 # now we need to add the '^' sign
235 aliases = ["invalid-value", ]
236 self._pckdict[b"acceptable_aliases"] = \
237 ("\n".join(aliases)).encode("utf-8")
238 try:
239 self._import()
240 except ValueError, e:
241 print(format_exc())
242 self.fail("Invalid value '%s' caused a crash" % e)
243 alias_set = IAcceptableAliasSet(self._mlist)
244 self.assertEqual(sorted(alias_set.aliases),
245 [ ("^" + a) for a in aliases ])
246
247 def test_acceptable_aliases_as_list(self):
248 # in some versions of the pickle, it can be a list, not a string
249 # (seen in the wild)
250 aliases = [b"alias1@example.com", b"alias2@exemple.com" ]
251 self._pckdict[b"acceptable_aliases"] = aliases
252 try:
253 self._import()
254 except AttributeError:
255 print(format_exc())
256 self.fail("Import does not handle acceptable_aliases as list")
257 alias_set = IAcceptableAliasSet(self._mlist)
258 self.assertEqual(sorted(alias_set.aliases), aliases)
259
260 def test_info_non_ascii(self):
261 # info can contain non-ascii chars
262 info = 'O idioma aceito \xe9 somente Portugu\xeas do Brasil'
263 self._pckdict[b"info"] = info.encode("utf-8")
264 self._import()
265 self.assertEqual(self._mlist.info, info,
266 "Encoding to UTF-8 is not handled")
267 # test fallback to ascii with replace
268 self._pckdict[b"info"] = info.encode("iso-8859-1")
269 self._import()
270 self.assertEqual(self._mlist.info,
271 unicode(self._pckdict[b"info"], "ascii", "replace"),
272 "We don't fall back to replacing non-ascii chars")
273
274 def test_preferred_language(self):
275 self._pckdict[b"preferred_language"] = b'ja'
276 english = getUtility(ILanguageManager).get('en')
277 japanese = getUtility(ILanguageManager).get('ja')
278 self.assertEqual(self._mlist.preferred_language, english)
279 self._import()
280 self.assertEqual(self._mlist.preferred_language, japanese)
281
282 def test_preferred_language_unknown_previous(self):
283 # when the previous language is unknown, it should not fail
284 self._mlist._preferred_language = 'xx' # non-existant
285 self._import()
286 english = getUtility(ILanguageManager).get('en')
287 self.assertEqual(self._mlist.preferred_language, english)
288
289 def test_new_language(self):
290 self._pckdict[b"preferred_language"] = b'xx_XX'
291 try:
292 self._import()
293 except Import21Error, e:
294 # check the message
295 self.assertTrue("[language.xx_XX]" in str(e))
296 else:
297 self.fail("Import21Error was not raised")
298
74299
75300
76301
77class TestArchiveImport(unittest.TestCase):302class TestArchiveImport(unittest.TestCase):
@@ -83,7 +308,10 @@
83308
84 def setUp(self):309 def setUp(self):
85 self._mlist = create_list('blank@example.com')310 self._mlist = create_list('blank@example.com')
86 self._mlist.archive_policy = 'INITIAL-TEST-VALUE'311 self._mlist.archive_policy = DummyEnum.val
312
313 def tearDown(self):
314 remove_list(self._mlist)
87315
88 def _do_test(self, pckdict, expected):316 def _do_test(self, pckdict, expected):
89 import_config_pck(self._mlist, pckdict)317 import_config_pck(self._mlist, pckdict)
@@ -123,3 +351,516 @@
123 # For some reason, the old list was missing an `archive_private` key.351 # For some reason, the old list was missing an `archive_private` key.
124 # For maximum safety, we treat this as private archiving.352 # For maximum safety, we treat this as private archiving.
125 self._do_test(dict(archive=True), ArchivePolicy.private)353 self._do_test(dict(archive=True), ArchivePolicy.private)
354
355
356
126357
358class TestFilterActionImport(unittest.TestCase):
359 # The mlist.filter_action enum values have changed. In Mailman 2.1 the
360 # order was 'Discard', 'Reject', 'Forward to List Owner', 'Preserve'.
361
362 layer = ConfigLayer
363
364 def setUp(self):
365 self._mlist = create_list('blank@example.com')
366 self._mlist.filter_action = DummyEnum.val
367
368 def tearDown(self):
369 remove_list(self._mlist)
370
371 def _do_test(self, original, expected):
372 import_config_pck(self._mlist, dict(filter_action=original))
373 self.assertEqual(self._mlist.filter_action, expected)
374
375 def test_discard(self):
376 self._do_test(0, FilterAction.discard)
377
378 def test_reject(self):
379 self._do_test(1, FilterAction.reject)
380
381 def test_forward(self):
382 self._do_test(2, FilterAction.forward)
383
384 def test_preserve(self):
385 self._do_test(3, FilterAction.preserve)
386
387
388
127389
390class TestMemberActionImport(unittest.TestCase):
391 # The mlist.default_member_action and mlist.default_nonmember_action enum
392 # values are different in Mailman 2.1, they have been merged into a
393 # single enum in Mailman 3
394 # For default_member_action, which used to be called
395 # member_moderation_action, the values were:
396 # 0==Hold, 1=Reject, 2==Discard
397 # For default_nonmember_action, which used to be called
398 # generic_nonmember_action, the values were:
399 # 0==Accept, 1==Hold, 2==Reject, 3==Discard
400
401 layer = ConfigLayer
402
403 def setUp(self):
404 self._mlist = create_list('blank@example.com')
405 self._mlist.default_member_action = DummyEnum.val
406 self._mlist.default_nonmember_action = DummyEnum.val
407 self._pckdict = dict(
408 member_moderation_action=DummyEnum.val,
409 generic_nonmember_action=DummyEnum.val,
410 )
411
412 def tearDown(self):
413 remove_list(self._mlist)
414
415 def _do_test(self, expected):
416 import_config_pck(self._mlist, self._pckdict)
417 for key, value in expected.iteritems():
418 self.assertEqual(getattr(self._mlist, key), value)
419
420 def test_member_hold(self):
421 self._pckdict[b"member_moderation_action"] = 0
422 self._do_test(dict(default_member_action=Action.hold))
423
424 def test_member_reject(self):
425 self._pckdict[b"member_moderation_action"] = 1
426 self._do_test(dict(default_member_action=Action.reject))
427
428 def test_member_discard(self):
429 self._pckdict[b"member_moderation_action"] = 2
430 self._do_test(dict(default_member_action=Action.discard))
431
432 def test_nonmember_accept(self):
433 self._pckdict[b"generic_nonmember_action"] = 0
434 self._do_test(dict(default_nonmember_action=Action.accept))
435
436 def test_nonmember_hold(self):
437 self._pckdict[b"generic_nonmember_action"] = 1
438 self._do_test(dict(default_nonmember_action=Action.hold))
439
440 def test_nonmember_reject(self):
441 self._pckdict[b"generic_nonmember_action"] = 2
442 self._do_test(dict(default_nonmember_action=Action.reject))
443
444 def test_nonmember_discard(self):
445 self._pckdict[b"generic_nonmember_action"] = 3
446 self._do_test(dict(default_nonmember_action=Action.discard))
447
448
449
128450
451class TestConvertToURI(unittest.TestCase):
452 # The following values were plain text, and are now URIs in Mailman 3:
453 # - welcome_message_uri
454 # - goodbye_message_uri
455 # - header_uri
456 # - footer_uri
457 # - digest_header_uri
458 # - digest_footer_uri
459 #
460 # The templates contain variables that must be replaced:
461 # - %(real_name)s -> %(display_name)s
462 # - %(real_name)s@%(host_name)s -> %(fqdn_listname)s
463 # - %(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s -> %(listinfo_uri)s
464
465 layer = ConfigLayer
466
467 def setUp(self):
468 self._mlist = create_list('blank@example.com')
469 self._conf_mapping = dict(
470 welcome_msg="welcome_message_uri",
471 goodbye_msg="goodbye_message_uri",
472 msg_header="header_uri",
473 msg_footer="footer_uri",
474 digest_header="digest_header_uri",
475 digest_footer="digest_footer_uri",
476 )
477 self._pckdict = dict()
478 #self._pckdict = {
479 # "preferred_language": "XX", # templates are lang-specific
480 #}
481
482 def tearDown(self):
483 remove_list(self._mlist)
484
485 def test_text_to_uri(self):
486 for oldvar, newvar in self._conf_mapping.iteritems():
487 self._pckdict[str(oldvar)] = b"TEST VALUE"
488 import_config_pck(self._mlist, self._pckdict)
489 newattr = getattr(self._mlist, newvar)
490 text = decorate(self._mlist, newattr)
491 self.assertEqual(text, "TEST VALUE",
492 "Old variable %s was not properly imported to %s"
493 % (oldvar, newvar))
494
495 def test_substitutions(self):
496 test_text = ("UNIT TESTING %(real_name)s mailing list\n"
497 "%(real_name)s@%(host_name)s\n"
498 "%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s")
499 expected_text = ("UNIT TESTING $display_name mailing list\n"
500 "$fqdn_listname\n"
501 "$listinfo_uri")
502 for oldvar, newvar in self._conf_mapping.iteritems():
503 self._pckdict[str(oldvar)] = str(test_text)
504 import_config_pck(self._mlist, self._pckdict)
505 newattr = getattr(self._mlist, newvar)
506 template_uri = expand(newattr, dict(
507 listname=self._mlist.fqdn_listname,
508 language=self._mlist.preferred_language.code,
509 ))
510 loader = getUtility(ITemplateLoader)
511 text = loader.get(template_uri)
512 self.assertEqual(text, expected_text,
513 "Old variables were not converted for %s" % newvar)
514
515 def test_keep_default(self):
516 # If the value was not changed from MM2.1's default, don't import it
517 default_msg_footer = (
518 "_______________________________________________\n"
519 "%(real_name)s mailing list\n"
520 "%(real_name)s@%(host_name)s\n"
521 "%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s"
522 )
523 for oldvar in ("msg_footer", "digest_footer"):
524 newvar = self._conf_mapping[oldvar]
525 self._pckdict[str(oldvar)] = str(default_msg_footer)
526 old_value = getattr(self._mlist, newvar)
527 import_config_pck(self._mlist, self._pckdict)
528 new_value = getattr(self._mlist, newvar)
529 self.assertEqual(old_value, new_value,
530 "Default value was not preserved for %s" % newvar)
531
532 def test_keep_default_if_fqdn_changed(self):
533 # Use case: importing the old a@ex.com into b@ex.com
534 # We can't check if it changed from the default
535 # -> don't import, we may do more harm than good and it's easy to
536 # change if needed
537 test_value = b"TEST-VALUE"
538 for oldvar, newvar in self._conf_mapping.iteritems():
539 self._mlist.mail_host = "example.com"
540 self._pckdict[b"mail_host"] = b"test.example.com"
541 self._pckdict[str(oldvar)] = test_value
542 old_value = getattr(self._mlist, newvar)
543 import_config_pck(self._mlist, self._pckdict)
544 new_value = getattr(self._mlist, newvar)
545 self.assertEqual(old_value, new_value,
546 "Default value was not preserved for %s" % newvar)
547
548 def test_unicode(self):
549 # non-ascii templates
550 for oldvar in self._conf_mapping:
551 self._pckdict[str(oldvar)] = b"Ol\xe1!"
552 try:
553 import_config_pck(self._mlist, self._pckdict)
554 except UnicodeDecodeError, e:
555 print(format_exc())
556 self.fail(e)
557 for oldvar, newvar in self._conf_mapping.iteritems():
558 newattr = getattr(self._mlist, newvar)
559 text = decorate(self._mlist, newattr)
560 expected = u'Ol\ufffd!'
561 self.assertEqual(text, expected)
562
563 def test_unicode_in_default(self):
564 # What if the default template is already in UTF-8? (like if you import twice)
565 footer = b'\xe4\xb8\xad $listinfo_uri'
566 footer_path = os.path.join(config.VAR_DIR, "templates", "lists",
567 "blank@example.com", "en", "footer-generic.txt")
568 try:
569 os.makedirs(os.path.dirname(footer_path))
570 except OSError:
571 pass
572 with open(footer_path, "w") as footer_file:
573 footer_file.write(footer)
574 self._pckdict[b"msg_footer"] = b"NEW-VALUE"
575 import_config_pck(self._mlist, self._pckdict)
576 text = decorate(self._mlist, self._mlist.footer_uri)
577 self.assertEqual(text, 'NEW-VALUE')
578
579
129580
581class TestRosterImport(unittest.TestCase):
582
583 layer = ConfigLayer
584
585 def setUp(self):
586 self._mlist = create_list('blank@example.com')
587 self._pckdict = {
588 "members": {
589 "anne@example.com": 0,
590 "bob@example.com": b"bob@ExampLe.Com",
591 },
592 "digest_members": {
593 "cindy@example.com": 0,
594 "dave@example.com": b"dave@ExampLe.Com",
595 },
596 "passwords": {
597 "anne@example.com" : b"annepass",
598 "bob@example.com" : b"bobpass",
599 "cindy@example.com": b"cindypass",
600 "dave@example.com" : b"davepass",
601 },
602 "language": {
603 "anne@example.com" : b"fr",
604 "bob@example.com" : b"de",
605 "cindy@example.com": b"es",
606 "dave@example.com" : b"it",
607 },
608 "usernames": { # Usernames are unicode strings in the pickle
609 "anne@example.com" : "Anne",
610 "bob@example.com" : "Bob",
611 "cindy@example.com": "Cindy",
612 "dave@example.com" : "Dave",
613 },
614 "owner": [
615 "anne@example.com",
616 "emily@example.com",
617 ],
618 "moderator": [
619 "bob@example.com",
620 "fred@example.com",
621 ],
622 }
623 self._usermanager = getUtility(IUserManager)
624 language_manager = getUtility(ILanguageManager)
625 for code in self._pckdict["language"].values():
626 if code not in language_manager.codes:
627 language_manager.add(code, 'utf-8', code)
628
629 def tearDown(self):
630 remove_list(self._mlist)
631
632 def test_member(self):
633 import_config_pck(self._mlist, self._pckdict)
634 for name in ("anne", "bob", "cindy", "dave"):
635 addr = "%s@example.com" % name
636 self.assertTrue(
637 addr in [ a.email for a in self._mlist.members.addresses],
638 "Address %s was not imported" % addr)
639 self.assertTrue("anne@example.com" in [ a.email
640 for a in self._mlist.regular_members.addresses])
641 self.assertTrue("bob@example.com" in [ a.email
642 for a in self._mlist.regular_members.addresses])
643 self.assertTrue("cindy@example.com" in [ a.email
644 for a in self._mlist.digest_members.addresses])
645 self.assertTrue("dave@example.com" in [ a.email
646 for a in self._mlist.digest_members.addresses])
647
648 def test_original_email(self):
649 import_config_pck(self._mlist, self._pckdict)
650 bob = self._usermanager.get_address("bob@example.com")
651 self.assertEqual(bob.original_email, "bob@ExampLe.Com")
652 dave = self._usermanager.get_address("dave@example.com")
653 self.assertEqual(dave.original_email, "dave@ExampLe.Com")
654
655 def test_language(self):
656 import_config_pck(self._mlist, self._pckdict)
657 for name in ("anne", "bob", "cindy", "dave"):
658 addr = "%s@example.com" % name
659 member = self._mlist.members.get_member(addr)
660 self.assertTrue(member is not None,
661 "Address %s was not imported" % addr)
662 print(self._pckdict["language"])
663 print(member.preferred_language, member.preferred_language.code)
664 self.assertEqual(member.preferred_language.code,
665 self._pckdict["language"][addr])
666
667 def test_new_language(self):
668 self._pckdict[b"language"]["anne@example.com"] = b'xx_XX'
669 try:
670 import_config_pck(self._mlist, self._pckdict)
671 except Import21Error, e:
672 # check the message
673 self.assertTrue("[language.xx_XX]" in str(e))
674 else:
675 self.fail("Import21Error was not raised")
676
677 def test_username(self):
678 import_config_pck(self._mlist, self._pckdict)
679 for name in ("anne", "bob", "cindy", "dave"):
680 addr = "%s@example.com" % name
681 user = self._usermanager.get_user(addr)
682 address = self._usermanager.get_address(addr)
683 self.assertTrue(user is not None,
684 "User %s was not imported" % addr)
685 self.assertTrue(address is not None,
686 "Address %s was not imported" % addr)
687 display_name = self._pckdict["usernames"][addr]
688 self.assertEqual(user.display_name, display_name,
689 "The display name was not set for User %s" % addr)
690 self.assertEqual(address.display_name, display_name,
691 "The display name was not set for Address %s" % addr)
692
693 def test_owner(self):
694 import_config_pck(self._mlist, self._pckdict)
695 for name in ("anne", "emily"):
696 addr = "%s@example.com" % name
697 self.assertTrue(
698 addr in [ a.email for a in self._mlist.owners.addresses ],
699 "Address %s was not imported as owner" % addr)
700 self.assertFalse("emily@example.com" in
701 [ a.email for a in self._mlist.members.addresses ],
702 "Address emily@ was wrongly added to the members list")
703
704 def test_moderator(self):
705 import_config_pck(self._mlist, self._pckdict)
706 for name in ("bob", "fred"):
707 addr = "%s@example.com" % name
708 self.assertTrue(
709 addr in [ a.email for a in self._mlist.moderators.addresses ],
710 "Address %s was not imported as moderator" % addr)
711 self.assertFalse("fred@example.com" in
712 [ a.email for a in self._mlist.members.addresses ],
713 "Address fred@ was wrongly added to the members list")
714
715 def test_password(self):
716 #self.anne.password = config.password_context.encrypt('abc123')
717 import_config_pck(self._mlist, self._pckdict)
718 for name in ("anne", "bob", "cindy", "dave"):
719 addr = "%s@example.com" % name
720 user = self._usermanager.get_user(addr)
721 self.assertTrue(user is not None,
722 "Address %s was not imported" % addr)
723 self.assertEqual(user.password, b'{plaintext}%spass' % name,
724 "Password for %s was not imported" % addr)
725
726 def test_same_user(self):
727 # Adding the address of an existing User must not create another user
728 user = self._usermanager.create_user('anne@example.com', 'Anne')
729 user.register("bob@example.com") # secondary email
730 import_config_pck(self._mlist, self._pckdict)
731 member = self._mlist.members.get_member('bob@example.com')
732 self.assertEqual(member.user, user)
733
734 def test_owner_and_moderator_not_lowercase(self):
735 # In the v2.1 pickled dict, the owner and moderator lists are not
736 # necessarily lowercased already
737 self._pckdict["owner"] = [b"Anne@example.com"]
738 self._pckdict["moderator"] = [b"Anne@example.com"]
739 try:
740 import_config_pck(self._mlist, self._pckdict)
741 except AssertionError:
742 print(format_exc())
743 self.fail("The address was not lowercased")
744 self.assertTrue("anne@example.com" in
745 [ a.email for a in self._mlist.owners.addresses ])
746 self.assertTrue("anne@example.com" in
747 [ a.email for a in self._mlist.moderators.addresses])
748
749 def test_address_already_exists_but_no_user(self):
750 # An address already exists, but it is not linked to a user nor
751 # subscribed
752 anne_addr = Address("anne@example.com", "Anne")
753 Store.of(self._mlist).add(anne_addr)
754 try:
755 import_config_pck(self._mlist, self._pckdict)
756 except ExistingAddressError:
757 print(format_exc())
758 self.fail("existing address was not checked")
759 anne = self._usermanager.get_user("anne@example.com")
760 self.assertTrue(anne.controls("anne@example.com"))
761 self.assertTrue(anne_addr in self._mlist.regular_members.addresses)
762
763 def test_address_already_subscribed_but_no_user(self):
764 # An address is already subscribed, but it is not linked to a user
765 anne_addr = Address("anne@example.com", "Anne")
766 self._mlist.subscribe(anne_addr)
767 try:
768 import_config_pck(self._mlist, self._pckdict)
769 except ExistingAddressError:
770 print(format_exc())
771 self.fail("existing address was not checked")
772 anne = self._usermanager.get_user("anne@example.com")
773 self.assertTrue(anne.controls("anne@example.com"))
774
775
776
777
130778
779class TestPreferencesImport(unittest.TestCase):
780
781 layer = ConfigLayer
782
783 def setUp(self):
784 self._mlist = create_list('blank@example.com')
785 self._pckdict = dict(
786 members={ "anne@example.com": 0 },
787 user_options=dict(),
788 delivery_status=dict(),
789 )
790 self._usermanager = getUtility(IUserManager)
791
792 def tearDown(self):
793 remove_list(self._mlist)
794
795 def _do_test(self, oldvalue, expected):
796 self._pckdict["user_options"]["anne@example.com"] = oldvalue
797 import_config_pck(self._mlist, self._pckdict)
798 user = self._usermanager.get_user("anne@example.com")
799 self.assertTrue(user is not None, "User was not imported")
800 member = self._mlist.members.get_member("anne@example.com")
801 self.assertTrue(member is not None, "Address was not subscribed")
802 for exp_name, exp_val in expected.iteritems():
803 try:
804 currentval = getattr(member, exp_name)
805 except AttributeError:
806 # hide_address has no direct getter
807 currentval = getattr(member.preferences, exp_name)
808 self.assertEqual(currentval, exp_val,
809 "Preference %s was not imported" % exp_name)
810 # XXX: should I check that other params are still equal to
811 # mailman.core.constants.system_preferences ?
812
813 def test_acknowledge_posts(self):
814 # AcknowledgePosts
815 self._do_test(4, dict(acknowledge_posts=True))
816
817 def test_hide_address(self):
818 # ConcealSubscription
819 self._do_test(16, dict(hide_address=True))
820
821 def test_receive_own_postings(self):
822 # DontReceiveOwnPosts
823 self._do_test(2, dict(receive_own_postings=False))
824
825 def test_receive_list_copy(self):
826 # DontReceiveDuplicates
827 self._do_test(256, dict(receive_list_copy=False))
828
829 def test_digest_plain(self):
830 # Digests & DisableMime
831 self._pckdict["digest_members"] = self._pckdict["members"].copy()
832 self._pckdict["members"] = dict()
833 self._do_test(8, dict(delivery_mode=DeliveryMode.plaintext_digests))
834
835 def test_digest_mime(self):
836 # Digests & not DisableMime
837 self._pckdict["digest_members"] = self._pckdict["members"].copy()
838 self._pckdict["members"] = dict()
839 self._do_test(0, dict(delivery_mode=DeliveryMode.mime_digests))
840
841 def test_delivery_status(self):
842 # look for the pckdict["delivery_status"] key which will look like
843 # (status, time) where status is among the following:
844 # ENABLED = 0 # enabled
845 # UNKNOWN = 1 # legacy disabled
846 # BYUSER = 2 # disabled by user choice
847 # BYADMIN = 3 # disabled by admin choice
848 # BYBOUNCE = 4 # disabled by bounces
849 for oldval, expected in enumerate((DeliveryStatus.enabled,
850 DeliveryStatus.unknown, DeliveryStatus.by_user,
851 DeliveryStatus.by_moderator, DeliveryStatus.by_bounces)):
852 self._pckdict["delivery_status"]["anne@example.com"] = (oldval, 0)
853 import_config_pck(self._mlist, self._pckdict)
854 member = self._mlist.members.get_member("anne@example.com")
855 self.assertTrue(member is not None, "Address was not subscribed")
856 self.assertEqual(member.delivery_status, expected)
857 member.unsubscribe()
858
859 def test_moderate(self):
860 # Option flag Moderate is translated to
861 # member.moderation_action = Action.hold
862 self._do_test(128, dict(moderation_action=Action.hold))
863
864 def test_multiple_options(self):
865 # DontReceiveDuplicates & DisableMime & SuppressPasswordReminder
866 self._pckdict[b"digest_members"] = self._pckdict[b"members"].copy()
867 self._pckdict[b"members"] = dict()
868 self._do_test(296, dict(
869 receive_list_copy=False,
870 delivery_mode=DeliveryMode.plaintext_digests,
871 ))