1 # Copyright (C) 2010-2013 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Importer routines."""
20 from __future__
import absolute_import
, print_function
, unicode_literals
32 from urllib2
import URLError
34 from mailman
.config
import config
35 from mailman
.core
.errors
import MailmanError
36 from mailman
.interfaces
.action
import FilterAction
, Action
37 from mailman
.interfaces
.autorespond
import ResponseAction
38 from mailman
.interfaces
.digests
import DigestFrequency
39 from mailman
.interfaces
.mailinglist
import Personalization
, ReplyToMunging
40 from mailman
.interfaces
.nntp
import NewsgroupModeration
41 from mailman
.interfaces
.archiver
import ArchivePolicy
42 from mailman
.interfaces
.bans
import IBanManager
43 from mailman
.interfaces
.mailinglist
import IAcceptableAliasSet
44 from mailman
.interfaces
.bounce
import UnrecognizedBounceDisposition
45 from mailman
.interfaces
.usermanager
import IUserManager
46 from mailman
.interfaces
.member
import DeliveryMode
, DeliveryStatus
, MemberRole
47 from mailman
.interfaces
.languages
import ILanguageManager
48 from mailman
.handlers
.decorate
import decorate
, decorate_template
49 from mailman
.utilities
.i18n
import search
50 from zope
.component
import getUtility
54 class Import21Error(MailmanError
):
58 def str_to_unicode(value
):
59 # Convert a string to unicode when the encoding is not declared
60 if isinstance(value
, unicode):
62 for encoding
in ("ascii", "utf-8"):
64 return unicode(value
, encoding
)
65 except UnicodeDecodeError:
67 # we did our best, use replace
68 return unicode(value
, 'ascii', 'replace')
71 def seconds_to_delta(value
):
72 return datetime
.timedelta(seconds
=value
)
75 def days_to_delta(value
):
76 return datetime
.timedelta(days
=value
)
79 def list_members_to_unicode(value
):
80 return [ unicode(item
) for item
in value
]
83 def filter_action_mapping(value
):
84 # The filter_action enum values have changed. In Mailman 2.1 the order was
85 # 'Discard', 'Reject', 'Forward to List Owner', 'Preserve'.
86 # In 3.0 it's 'hold', 'reject', 'discard', 'accept', 'defer', 'forward',
89 return FilterAction
.discard
91 return FilterAction
.reject
93 return FilterAction
.forward
95 return FilterAction
.preserve
97 raise ValueError("Unknown filter_action value: %s" % value
)
100 def member_action_mapping(value
):
101 # The mlist.default_member_action and mlist.default_nonmember_action enum
102 # values are different in Mailman 2.1, because they have been merged into a
103 # single enum in Mailman 3
104 # For default_member_action, which used to be called
105 # member_moderation_action, the values were:
106 # 0==Hold, 1=Reject, 2==Discard
112 return Action
.discard
113 def nonmember_action_mapping(value
):
114 # For default_nonmember_action, which used to be called
115 # generic_nonmember_action, the values were:
116 # 0==Accept, 1==Hold, 2==Reject, 3==Discard
124 return Action
.discard
127 def unicode_to_string(value
):
128 return str(value
) if value
is not None else None
131 def check_language_code(code
):
135 if code
not in getUtility(ILanguageManager
):
136 msg
= """Missing language: {0}
137 You must add a section describing this language in your mailman.cfg file.
138 This section should look like this:
140 # The English name for the language.
141 description: CHANGE ME
142 # And the default character set for the language.
144 # Whether the language is enabled or not.
147 raise Import21Error(msg
)
151 # Attributes in Mailman 2 which have a different type in Mailman 3.
153 autorespond_owner
=ResponseAction
,
154 autorespond_postings
=ResponseAction
,
155 autorespond_requests
=ResponseAction
,
156 autoresponse_grace_period
=days_to_delta
,
157 bounce_info_stale_after
=seconds_to_delta
,
158 bounce_you_are_disabled_warnings_interval
=seconds_to_delta
,
159 digest_volume_frequency
=DigestFrequency
,
160 filter_action
=filter_action_mapping
,
161 newsgroup_moderation
=NewsgroupModeration
,
162 personalize
=Personalization
,
163 reply_goes_to_list
=ReplyToMunging
,
164 filter_types
=list_members_to_unicode
,
165 pass_types
=list_members_to_unicode
,
166 filter_extensions
=list_members_to_unicode
,
167 pass_extensions
=list_members_to_unicode
,
168 forward_unrecognized_bounces_to
=UnrecognizedBounceDisposition
,
169 default_member_action
=member_action_mapping
,
170 default_nonmember_action
=nonmember_action_mapping
,
171 moderator_password
=unicode_to_string
,
172 preferred_language
=check_language_code
,
176 # Attribute names in Mailman 2 which are renamed in Mailman 3.
177 NAME_MAPPINGS
= dict(
178 include_list_post_header
='allow_list_posts',
179 real_name
='display_name',
180 last_post_time
='last_post_at',
181 autoresponse_graceperiod
='autoresponse_grace_period',
182 autorespond_admin
='autorespond_owner',
183 autoresponse_admin_text
='autoresponse_owner_text',
184 filter_mime_types
='filter_types',
185 pass_mime_types
='pass_types',
186 filter_filename_extensions
='filter_extensions',
187 pass_filename_extensions
='pass_extensions',
188 bounce_processing
='process_bounces',
189 bounce_unrecognized_goes_to_list_owner
='forward_unrecognized_bounces_to',
190 mod_password
='moderator_password',
191 news_moderation
='newsgroup_moderation',
192 news_prefix_subject_too
='nntp_prefix_subject_too',
193 send_welcome_msg
='send_welcome_message',
194 send_goodbye_msg
='send_goodbye_message',
195 member_moderation_action
='default_member_action',
196 generic_nonmember_action
='default_nonmember_action',
206 def import_config_pck(mlist
, config_dict
):
207 """Apply a config.pck configuration dictionary to a mailing list.
209 :param mlist: The mailing list.
210 :type mlist: IMailingList
211 :param config_dict: The Mailman 2.1 configuration dictionary.
212 :type config_dict: dict
214 for key
, value
in config_dict
.items():
215 # Some attributes must not be directly imported
218 # Some attributes from Mailman 2 were renamed in Mailman 3.
219 key
= NAME_MAPPINGS
.get(key
, key
)
220 # Handle the simple case where the key is an attribute of the
221 # IMailingList and the types are the same (modulo 8-bit/unicode
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", ):
226 if isinstance(value
, str):
227 value
= str_to_unicode(value
)
228 # Some types require conversion.
229 converter
= TYPES
.get(key
)
230 if converter
is not None:
231 value
= converter(value
)
233 setattr(mlist
, key
, value
)
235 print('Type conversion error:', key
, file=sys
.stderr
)
237 # Handle the archiving policy. In MM2.1 there were two boolean options
238 # but only three of the four possible states were valid. Now there's just
240 if config_dict
.get('archive'):
241 # For maximum safety, if for some strange reason there's no
242 # archive_private key, treat the list as having private archives.
243 if config_dict
.get('archive_private', True):
244 mlist
.archive_policy
= ArchivePolicy
.private
246 mlist
.archive_policy
= ArchivePolicy
.public
248 mlist
.archive_policy
= ArchivePolicy
.never
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
:
260 addr
= str_to_unicode(addr
)
262 IAcceptableAliasSet(mlist
).add(addr
)
264 IAcceptableAliasSet(mlist
).add("^" + addr
)
265 # Handle conversion to URIs
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",
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",
281 for oldvar
, newvar
in convert_to_uri
.iteritems():
282 default_value
= getattr(mlist
, newvar
)
283 if not default_value
:
285 # Check if the value changed from the default
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
294 defaults
[newvar
] = (default_value
, default_text
)
295 for oldvar
, newvar
in convert_to_uri
.iteritems():
296 if oldvar
not in config_dict
:
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
307 expanded_text
= decorate_template(mlist
, text
)
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
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/"
320 filename
= default_value
.rpartition("/")[2]
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]
327 os
.makedirs(os
.path
.dirname(filepath
))
329 if e
.errno
!= 17: # Already exists
331 with
open(filepath
, "w") as template
:
332 template
.write(text
.encode('utf-8'))
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", []),
339 import_roster(mlist
, config_dict
, config_dict
.get("moderator", []),
340 MemberRole
.moderator
)
344 def import_roster(mlist
, config_dict
, members
, role
):
346 Import members lists from a config.pck configuration dictionary to a
349 :param mlist: The mailing list.
350 :type mlist: IMailingList
351 :param config_dict: The Mailman 2.1 configuration dictionary.
352 :type config_dict: dict
353 :param members: The members list to import
355 :param role: The MemberRole to import them as
356 :type role: MemberRole enum
358 usermanager
= getUtility(IUserManager
)
359 for email
in members
:
360 # for owners and members, the emails can have a mixed case, so
362 email
= str_to_unicode(email
).lower()
363 roster
= mlist
.get_roster(role
)
364 if roster
.get_member(email
) is not None:
365 print("%s is already imported with role %s" % (email
, role
),
368 address
= usermanager
.get_address(email
)
369 user
= usermanager
.get_user(email
)
371 user
= usermanager
.create_user()
374 merged_members
.update(config_dict
.get("members", {}))
375 merged_members
.update(config_dict
.get("digest_members", {}))
376 if merged_members
.get(email
, 0) != 0:
377 original_email
= str_to_unicode(merged_members
[email
])
379 original_email
= email
380 address
= usermanager
.create_address(original_email
)
381 address
.verified_on
= datetime
.datetime
.now()
383 mlist
.subscribe(address
, role
)
384 member
= roster
.get_member(email
)
385 assert member
is not None
386 prefs
= config_dict
.get("user_options", {}).get(email
, 0)
387 if email
in config_dict
.get("members", {}):
388 member
.preferences
.delivery_mode
= DeliveryMode
.regular
389 elif email
in config_dict
.get("digest_members", {}):
390 if prefs
& 8: # DisableMime
391 member
.preferences
.delivery_mode
= DeliveryMode
.plaintext_digests
393 member
.preferences
.delivery_mode
= DeliveryMode
.mime_digests
395 # probably not adding a member role here
397 if email
in config_dict
.get("language", {}):
398 member
.preferences
.preferred_language
= \
399 check_language_code(config_dict
["language"][email
])
400 # if the user already exists, display_name and password will be
402 if email
in config_dict
.get("usernames", {}):
403 address
.display_name
= \
404 str_to_unicode(config_dict
["usernames"][email
])
405 user
.display_name
= \
406 str_to_unicode(config_dict
["usernames"][email
])
407 if email
in config_dict
.get("passwords", {}):
408 user
.password
= config
.password_context
.encrypt(
409 config_dict
["passwords"][email
])
411 oldds
= config_dict
.get("delivery_status", {}).get(email
, (0, 0))[0]
413 member
.preferences
.delivery_status
= DeliveryStatus
.enabled
415 member
.preferences
.delivery_status
= DeliveryStatus
.unknown
417 member
.preferences
.delivery_status
= DeliveryStatus
.by_user
419 member
.preferences
.delivery_status
= DeliveryStatus
.by_moderator
421 member
.preferences
.delivery_status
= DeliveryStatus
.by_bounces
424 member
.moderation_action
= Action
.hold
426 member
.preferences
.acknowledge_posts
= bool(prefs
& 4) # AcknowledgePosts
427 member
.preferences
.hide_address
= bool(prefs
& 16) # ConcealSubscription
428 member
.preferences
.receive_own_postings
= not bool(prefs
& 2) # DontReceiveOwnPosts
429 member
.preferences
.receive_list_copy
= not bool(prefs
& 256) # DontReceiveDuplicates