diff options
| author | Barry Warsaw | 2010-12-29 23:54:08 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2010-12-29 23:54:08 -0500 |
| commit | 534e90fea33c52585c74fa9127cca8b70178d5e0 (patch) | |
| tree | 3a5d4088b5af1a4b310dffba711389ac67792dd2 /src | |
| parent | a31184862fc52a3c38059f832d533b137135c1f9 (diff) | |
| download | mailman-534e90fea33c52585c74fa9127cca8b70178d5e0.tar.gz mailman-534e90fea33c52585c74fa9127cca8b70178d5e0.tar.zst mailman-534e90fea33c52585c74fa9127cca8b70178d5e0.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/app/docs/chains.txt | 8 | ||||
| -rw-r--r-- | src/mailman/chains/builtin.py | 7 | ||||
| -rw-r--r-- | src/mailman/chains/docs/moderation.txt | 82 | ||||
| -rw-r--r-- | src/mailman/chains/moderation.py (renamed from src/mailman/chains/membermod.py) | 53 | ||||
| -rw-r--r-- | src/mailman/database/mailman.sql | 6 | ||||
| -rw-r--r-- | src/mailman/interfaces/mailinglist.py | 24 | ||||
| -rw-r--r-- | src/mailman/interfaces/member.py | 5 | ||||
| -rw-r--r-- | src/mailman/interfaces/roster.py | 2 | ||||
| -rw-r--r-- | src/mailman/model/docs/membership.txt | 254 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 5 | ||||
| -rw-r--r-- | src/mailman/model/member.py | 19 | ||||
| -rw-r--r-- | src/mailman/model/roster.py | 8 | ||||
| -rw-r--r-- | src/mailman/pipeline/moderate.py | 173 | ||||
| -rw-r--r-- | src/mailman/queue/docs/incoming.txt | 6 | ||||
| -rw-r--r-- | src/mailman/rest/configuration.py | 4 | ||||
| -rw-r--r-- | src/mailman/rest/docs/configuration.txt | 18 | ||||
| -rw-r--r-- | src/mailman/rules/docs/moderation.txt | 103 | ||||
| -rw-r--r-- | src/mailman/rules/docs/rules.txt | 3 | ||||
| -rw-r--r-- | src/mailman/rules/moderation.py | 49 | ||||
| -rw-r--r-- | src/mailman/styles/default.py | 4 | ||||
| -rw-r--r-- | src/mailman/utilities/importer.py | 1 |
21 files changed, 397 insertions, 437 deletions
diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt index 3242f684e..58f1dd2fd 100644 --- a/src/mailman/app/docs/chains.txt +++ b/src/mailman/app/docs/chains.txt @@ -316,9 +316,9 @@ all default rules. This message will end up in the `pipeline` queue. Subject: My first post Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - X-Mailman-Rule-Misses: approved; emergency; loop; administrivia; - implicit-dest; max-recipients; max-size; news-moderation; no-subject; - suspicious-header; member-moderation + X-Mailman-Rule-Misses: approved; emergency; loop; moderation; + administrivia; implicit-dest; max-recipients; max-size; + news-moderation; no-subject; suspicious-header <BLANKLINE> An important message. <BLANKLINE> @@ -337,7 +337,7 @@ hit and all rules that have missed. loop max-recipients max-size - member-moderation + moderation news-moderation no-subject suspicious-header diff --git a/src/mailman/chains/builtin.py b/src/mailman/chains/builtin.py index c81f6700f..48e4bd535 100644 --- a/src/mailman/chains/builtin.py +++ b/src/mailman/chains/builtin.py @@ -51,8 +51,9 @@ class BuiltInChain: ('approved', LinkAction.jump, 'accept'), ('emergency', LinkAction.jump, 'hold'), ('loop', LinkAction.jump, 'discard'), - # Do all of the following before deciding whether to hold the message - # for moderation. + # Determine whether the member or nonmember has an action shortcut. + ('moderation', LinkAction.jump, 'moderation'), + # Do all of the following before deciding whether to hold the message. ('administrivia', LinkAction.defer, None), ('implicit-dest', LinkAction.defer, None), ('max-recipients', LinkAction.defer, None), @@ -62,8 +63,6 @@ class BuiltInChain: ('suspicious-header', LinkAction.defer, None), # Now if any of the above hit, jump to the hold chain. ('any', LinkAction.jump, 'hold'), - # Hold the message if the sender is a moderated member. - ('member-moderation', LinkAction.jump, 'member-moderation'), # Take a detour through the header matching chain, which we'll create # later. ('truth', LinkAction.detour, 'header-match'), diff --git a/src/mailman/chains/docs/moderation.txt b/src/mailman/chains/docs/moderation.txt index c95b8cac4..33bf63df9 100644 --- a/src/mailman/chains/docs/moderation.txt +++ b/src/mailman/chains/docs/moderation.txt @@ -8,14 +8,32 @@ moderator approval. >>> mlist = create_list('test@example.com') +Members and nonmembers have a *moderation action* which can shortcut the +normal moderation checks. The built-in chain does just a few checks first, +such as seeing if the message has a matching `Approved:` header, or if the +emergency flag has been set on the mailing list, or whether a mail loop has +been detected. + +After those, the moderation action for the sender is checked. Members +generally have a `defer` action, meaning the normal moderation checks are +done, but it is also common for first-time posters to have a `hold` action, +meaning that their messages are held for moderator approval for a while. + +Nonmembers almost always have a `hold` action, though some mailing lists may +choose to set this default action to `discard`, meaning their posts would be +immediately thrown away. + +XXX What about default nonmember actions when the poster has not been +registered as a nonmember? + Member moderation ================= -Posts by list members are moderated if the member's moderation flag is set. -The default setting for the moderation flag of new members is determined by -the mailing list's settings. By default, a mailing list is not set to -moderate new member postings. +Posts by list members are moderated if the member's moderation action is not +deferred. The default setting for the moderation action of new members is +determined by the mailing list's settings. By default, a mailing list is not +set to moderate new member postings. >>> from mailman.app.membership import add_member >>> from mailman.interfaces.member import DeliveryMode @@ -23,8 +41,8 @@ moderate new member postings. ... DeliveryMode.regular, 'en') >>> member <Member: Anne <anne@example.com> on test@example.com as MemberRole.member> - >>> member.is_moderated - False + >>> print member.moderation_action + Action.defer In order to find out whether the message is held or accepted, we can subscribe to Zope events that are triggered on each case. @@ -68,6 +86,7 @@ built-in chain. No rules hit and so the message is accepted. approved emergency loop + moderation administrivia implicit-dest max-recipients @@ -75,12 +94,14 @@ built-in chain. No rules hit and so the message is accepted. news-moderation no-subject suspicious-header - member-moderation -However, when Anne's moderation flag is set, and the list's member moderation -action is set to `hold`, her post is held for moderation. +However, when Anne's moderation action is set to `hold`, her post is held for +moderator approval. :: + >>> from mailman.interfaces.action import Action + >>> member.moderation_action = Action.hold + >>> msg = message_from_string("""\ ... From: anne@example.com ... To: test@example.com @@ -89,33 +110,21 @@ action is set to `hold`, her post is held for moderation. ... This is a test. ... """) - >>> member.is_moderated = True - >>> print mlist.member_moderation_action - Action.hold - >>> process(mlist, msg, {}, 'built-in') <mailman.chains.hold.HoldNotification ...> <mailman.chains.hold.HoldChain ...> Subject: badger Hits: - member-moderation + moderation Misses: approved emergency loop - administrivia - implicit-dest - max-recipients - max-size - news-moderation - no-subject - suspicious-header The list's member moderation action can also be set to `discard`... :: - >>> from mailman.interfaces.action import Action - >>> mlist.member_moderation_action = Action.discard + >>> member.moderation_action = Action.discard >>> msg = message_from_string("""\ ... From: anne@example.com @@ -130,22 +139,15 @@ The list's member moderation action can also be set to `discard`... <mailman.chains.discard.DiscardChain ...> Subject: cougar Hits: - member-moderation + moderation Misses: approved emergency loop - administrivia - implicit-dest - max-recipients - max-size - news-moderation - no-subject - suspicious-header ... or `reject`. - >>> mlist.member_moderation_action = Action.reject + >>> member.moderation_action = Action.reject >>> msg = message_from_string("""\ ... From: anne@example.com @@ -160,18 +162,18 @@ The list's member moderation action can also be set to `discard`... <mailman.chains.reject.RejectChain ...> Subject: dingo Hits: - member-moderation + moderation Misses: approved emergency loop - administrivia - implicit-dest - max-recipients - max-size - news-moderation - no-subject - suspicious-header + + +Nonmembers +========== + +XXX + .. Clean up >>> zope.event.subscribers.remove(on_chain) diff --git a/src/mailman/chains/membermod.py b/src/mailman/chains/moderation.py index 4a220b59d..1b5ab47b6 100644 --- a/src/mailman/chains/membermod.py +++ b/src/mailman/chains/moderation.py @@ -15,18 +15,30 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. -"""Member moderation chain. +"""Moderation chain. -When a member's moderation flag is set, the built-in chain jumps to this -chain, which just checks the mailing list's member moderation action. Based -on this value, one of the normal termination chains is jumped to. +When a member or nonmember posting to the mailing list has a moderation action +that is not `defer`, the built-in chain jumps to this chain. This chain then +determines the disposition of the message based on the member's or nonmember's +moderation action. + +For example, these actions jump to the appropriate terminal chain: + + * accept - the message is immediately accepted + * hold - the message is held for moderator approval + * reject - the message is bounced + * discard - the message is immediately thrown away + +Note that if the moderation action is `defer` then the normal decisions are +made as to the disposition of the message. `defer` is the default for +members, while `hold` is the default for nonmembers. """ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ - 'MemberModerationChain', + 'ModerationChain', ] @@ -40,33 +52,38 @@ from mailman.interfaces.chain import IChain, LinkAction -class MemberModerationChain: +class ModerationChain: """Dynamically produce a link jumping to the appropriate terminal chain. The terminal chain will be one of the Accept, Hold, Discard, or Reject - chains, based on the mailing list's member moderation action setting. + chains, based on the member's or nonmember's moderation action setting. """ implements(IChain) - name = 'member-moderation' - description = _('Member moderation chain') - is_abstract = False + name = 'moderation' + description = _('Moderation chain') def get_links(self, mlist, msg, msgdata): """See `IChain`.""" + # Get the moderation action from the message metadata. It can only be + # one of the expected values (i.e. not Action.defer). See the + # moderation.py rule for details. This is stored in the metadata as a + # string so that it can be stored in the pending table. + action = Action[msgdata.get('moderation_action')] # defer and accept are not valid moderation actions. - jump_chains = { + jump_chain = { + Action.accept: 'accept', + Action.discard: 'discard', Action.hold: 'hold', Action.reject: 'reject', - Action.discard: 'discard', - } - chain_name = jump_chains.get(mlist.member_moderation_action) - assert chain_name is not None, ( - '{0}: Invalid member_moderation_action: {1}'.format( - mlist.fqdn_listname, mlist.member_moderation_action)) + }.get(action) + assert jump_chain is not None, ( + '{0}: Invalid moderation action: {1} for sender: {2}'.format( + mlist.fqdn_listname, action, + msgdata.get('moderation_sender', '(unknown)'))) truth = config.rules['truth'] - chain = config.chains[chain_name] + chain = config.chains[jump_chain] return iter([ Link(truth, LinkAction.jump, chain), ]) diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index 4ba71f8f0..cfe44c8e1 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -131,7 +131,8 @@ CREATE TABLE mailinglist ( filter_content BOOLEAN, collapse_alternatives BOOLEAN, convert_html_to_plaintext BOOLEAN, - default_member_moderation BOOLEAN, + default_member_action INTEGER, + default_nonmember_action INTEGER, description TEXT, digest_footer TEXT, digest_header TEXT, @@ -156,7 +157,6 @@ CREATE TABLE mailinglist ( max_days_to_hold INTEGER, max_message_size INTEGER, max_num_recipients INTEGER, - member_moderation_action INTEGER, member_moderation_notice TEXT, mime_is_default_digest BOOLEAN, moderator_password TEXT, @@ -199,7 +199,7 @@ CREATE TABLE member ( id INTEGER NOT NULL, role TEXT, mailing_list TEXT, - is_moderated BOOLEAN, + moderation_action INTEGER, address_id INTEGER, preferences_id INTEGER, PRIMARY KEY (id), diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 882afaf42..54cfa9d24 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -459,20 +459,24 @@ class IMailingList(Interface): # Moderation. - default_member_moderation = Attribute( - """Default moderation flag for new mailing list members. + default_member_action = Attribute( + """The default action to take for postings from members. - When an address is subscribed to the mailing list, this boolean - attribute sets the initial moderation flag value. When a member's - posts are moderated, they must first be approved by the mailing list - owner or moderator. + When an address is subscribed to the mailing list, this attribute sets + the initial moderation action (as an `Action`). When the action is + `Action.defer` (the default), then normal posting decisions are made. + When the action is `Action.accept`, the postings are accepted without + any other checks. """) - member_moderation_action = Attribute( - """Action to take when a moderated member posts to the mailing list. + default_nonmember_action = Attribute( + """The default action to take for postings from nonmembers. - This is applied when the sender is a member of the mailing list and - has their `is_moderated` flag set. + When a nonmember address posts to the mailing list, this attribute + sets the initial moderation action (as an `Action`). When the action + is `Action.defer` (the default), then normal posting decisions are + made. When the action is `Action.accept`, the postings are accepted + without any other checks. """) diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py index 51caa6f6f..feae1aa9d 100644 --- a/src/mailman/interfaces/member.py +++ b/src/mailman/interfaces/member.py @@ -69,6 +69,7 @@ class MemberRole(Enum): member = 1 owner = 2 moderator = 3 + nonmember = 4 @@ -132,8 +133,8 @@ class IMember(Interface): role = Attribute( """The role of this membership.""") - is_moderated = Attribute( - """True if the membership is moderated, otherwise False.""") + moderation_action = Attribute( + """The moderation action for this member as an `Action`.""") def unsubscribe(): """Unsubscribe (and delete) this member from the mailing list.""" diff --git a/src/mailman/interfaces/roster.py b/src/mailman/interfaces/roster.py index 01d81cd2a..a7592e807 100644 --- a/src/mailman/interfaces/roster.py +++ b/src/mailman/interfaces/roster.py @@ -30,7 +30,7 @@ from zope.interface import Interface, Attribute class IRoster(Interface): - """A roster is a collection of IMembers.""" + """A roster is a collection of `IMembers`.""" name = Attribute( """The name for this roster. diff --git a/src/mailman/model/docs/membership.txt b/src/mailman/model/docs/membership.txt index 1db2550f7..28d14f149 100644 --- a/src/mailman/model/docs/membership.txt +++ b/src/mailman/model/docs/membership.txt @@ -4,22 +4,33 @@ List memberships Users represent people in Mailman. Users control email addresses, and rosters are collections of members. A member gives an email address a role, such as -`member`, `administrator`, or `moderator`. Roster sets are collections of -rosters and a mailing list has a single roster set that contains all its -members, regardless of that member's role. +`member`, `administrator`, or `moderator`. Even nonmembers are represented by +a roster. + +Roster sets are collections of rosters and a mailing list has a single roster +set that contains all its members, regardless of that member's role. Mailing lists and roster sets have an indirect relationship, through the roster set's name. Roster also have names, but are related to roster sets by a more direct containment relationship. This is because it is possible to store mailing list data in a different database than user data. -When we create a mailing list, it starts out with no members... +When we create a mailing list, it starts out with no members. +:: - >>> mlist = create_list('_xtest@example.com') - >>> mlist - <mailing list "_xtest@example.com" at ...> - >>> sorted(member.address.address for member in mlist.members.members) - [] + >>> mlist = create_list('test@example.com') + + >>> def print_addresses(roster_addresses): + ... sorted_addresses = sorted( + ... address.address for address in roster_addresses) + ... if len(sorted_addresses) == 0: + ... print 'No addresses' + ... else: + ... for address in sorted_addresses: + ... print address + + >>> print_addresses(mlist.members.members) + No addresses >>> sorted(user.real_name for user in mlist.members.users) [] >>> sorted(address.address for member in mlist.members.addresses) @@ -27,8 +38,8 @@ When we create a mailing list, it starts out with no members... ...no owners... - >>> sorted(member.address.address for member in mlist.owners.members) - [] + >>> print_addresses(mlist.owners.members) + No addresses >>> sorted(user.real_name for user in mlist.owners.users) [] >>> sorted(address.address for member in mlist.owners.addresses) @@ -36,23 +47,31 @@ When we create a mailing list, it starts out with no members... ...no moderators... - >>> sorted(member.address.address for member in mlist.moderators.members) - [] + >>> print_addresses(mlist.moderators.members) + No addresses >>> sorted(user.real_name for user in mlist.moderators.users) [] >>> sorted(address.address for member in mlist.moderators.addresses) [] -...and no administrators. +...and no administrators... - >>> sorted(member.address.address - ... for member in mlist.administrators.members) - [] + >>> print_addresses(mlist.administrators.members) + No addresses >>> sorted(user.real_name for user in mlist.administrators.users) [] >>> sorted(address.address for member in mlist.administrators.addresses) [] +...and no nonmembers. + + >>> print_addresses(mlist.nonmembers.members) + No addresses + >>> sorted(user.real_name for user in mlist.nonmembers.users) + [] + >>> sorted(address.address for member in mlist.nonmembers.addresses) + [] + Administrators ============== @@ -68,8 +87,8 @@ no users in the user database yet. >>> user_1 = user_manager.create_user('aperson@example.com', 'Anne Person') >>> print user_1.real_name Anne Person - >>> sorted(address.address for address in user_1.addresses) - [u'aperson@example.com'] + >>> print_addresses(user_1.addresses) + aperson@example.com We can add Anne as an owner of the mailing list, by creating a member role for her. @@ -80,13 +99,13 @@ her. aperson@example.com >>> address_1.subscribe(mlist, MemberRole.owner) <Member: Anne Person <aperson@example.com> on - _xtest@example.com as MemberRole.owner> - >>> sorted(member.address.address for member in mlist.owners.members) - [u'aperson@example.com'] + test@example.com as MemberRole.owner> + >>> print_addresses(mlist.owners.members) + Anne Person <aperson@example.com> >>> sorted(user.real_name for user in mlist.owners.users) [u'Anne Person'] - >>> sorted(address.address for address in mlist.owners.addresses) - [u'aperson@example.com'] + >>> print_addresses(mlist.owners.addresses) + aperson@example.com Adding Anne as a list owner also makes her an administrator, but does not make her a moderator. Nor does it make her a member of the list. @@ -98,34 +117,35 @@ her a moderator. Nor does it make her a member of the list. >>> sorted(user.real_name for user in mlist.members.users) [] -We can add Ben as a moderator of the list, by creating a different member role +We can add Bart as a moderator of the list, by creating a different member role for him. - >>> user_2 = user_manager.create_user('bperson@example.com', 'Ben Person') + >>> user_2 = user_manager.create_user('bperson@example.com', 'Bart Person') >>> print user_2.real_name - Ben Person + Bart Person >>> address_2 = list(user_2.addresses)[0] >>> print address_2.address bperson@example.com >>> address_2.subscribe(mlist, MemberRole.moderator) - <Member: Ben Person <bperson@example.com> - on _xtest@example.com as MemberRole.moderator> - >>> sorted(member.address.address for member in mlist.moderators.members) - [u'bperson@example.com'] + <Member: Bart Person <bperson@example.com> + on test@example.com as MemberRole.moderator> + >>> print_addresses(mlist.moderators.members) + Bart Person <bperson@example.com> >>> sorted(user.real_name for user in mlist.moderators.users) - [u'Ben Person'] - >>> sorted(address.address for address in mlist.moderators.addresses) - [u'bperson@example.com'] + [u'Bart Person'] + >>> print_addresses(mlist.moderators.addresses) + bperson@example.com -Now, both Anne and Ben are list administrators. +Now, both Anne and Bart are list administrators. - >>> sorted(member.address.address - ... for member in mlist.administrators.members) - [u'aperson@example.com', u'bperson@example.com'] + >>> print_addresses(mlist.administrators.members) + Anne Person <aperson@example.com> + Bart Person <bperson@example.com> >>> sorted(user.real_name for user in mlist.administrators.users) - [u'Anne Person', u'Ben Person'] - >>> sorted(address.address for address in mlist.administrators.addresses) - [u'aperson@example.com', u'bperson@example.com'] + [u'Anne Person', u'Bart Person'] + >>> print_addresses(mlist.administrators.addresses) + aperson@example.com + bperson@example.com Members @@ -139,24 +159,24 @@ preference, then the user's preference, then the list's preference. Start without any member preference to see the system defaults. >>> user_3 = user_manager.create_user( - ... 'cperson@example.com', 'Claire Person') + ... 'cperson@example.com', 'Cris Person') >>> print user_3.real_name - Claire Person + Cris Person >>> address_3 = list(user_3.addresses)[0] >>> print address_3.address cperson@example.com >>> address_3.subscribe(mlist, MemberRole.member) - <Member: Claire Person <cperson@example.com> - on _xtest@example.com as MemberRole.member> + <Member: Cris Person <cperson@example.com> + on test@example.com as MemberRole.member> -Claire will be a regular delivery member but not a digest member. +Cris will be a regular delivery member but not a digest member. - >>> sorted(address.address for address in mlist.members.addresses) - [u'cperson@example.com'] - >>> sorted(address.address for address in mlist.regular_members.addresses) - [u'cperson@example.com'] - >>> sorted(address.address for address in mlist.digest_members.addresses) - [] + >>> print_addresses(mlist.members.addresses) + cperson@example.com + >>> print_addresses(mlist.regular_members.addresses) + cperson@example.com + >>> print_addresses(mlist.digest_members.addresses) + No addresses It's easy to make the list administrators members of the mailing list too. @@ -166,15 +186,54 @@ It's easy to make the list administrators members of the mailing list too. ... members.append(member) >>> sorted(members, key=lambda m: m.address.address) [<Member: Anne Person <aperson@example.com> on - _xtest@example.com as MemberRole.member>, - <Member: Ben Person <bperson@example.com> on - _xtest@example.com as MemberRole.member>] - >>> sorted(address.address for address in mlist.members.addresses) - [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.address for address in mlist.regular_members.addresses) - [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.address for address in mlist.digest_members.addresses) - [] + test@example.com as MemberRole.member>, + <Member: Bart Person <bperson@example.com> on + test@example.com as MemberRole.member>] + >>> print_addresses(mlist.members.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + >>> print_addresses(mlist.regular_members.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + >>> print_addresses(mlist.digest_members.addresses) + No addresses + + +Nonmembers +========== + +Nonmembers are used to represent people who have posted to the mailing list +but are not subscribed to the mailing list. These may be legitimate users who +have found the mailing list and wish to interact without a direct +subscription. It may also be spammers who should never be allowed to contact +the mailing list. Because all the same moderation rules can be applied to +nonmembers, we represent them as the same type of object but with a different +role. + + >>> user_6 = user_manager.create_user('fperson@example.com', 'Fred') + >>> address_6 = list(user_6.addresses)[0] + >>> member_6 = address_6.subscribe(mlist, MemberRole.nonmember) + >>> member_6 + <Member: Fred <fperson@example.com> on test@example.com + as MemberRole.nonmember> + >>> print_addresses(mlist.nonmembers.addresses) + fperson@example.com + +Nonmembers do not get delivery of any messages. See that Fred does not show +up in any of the delivery rosters. + + >>> print_addresses(mlist.members.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + >>> print_addresses(mlist.regular_members.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + >>> print_addresses(mlist.digest_members.addresses) + No addresses Finding members @@ -185,13 +244,16 @@ text email address by using the ``IRoster.get_member()`` method. >>> mlist.owners.get_member('aperson@example.com') <Member: Anne Person <aperson@example.com> on - _xtest@example.com as MemberRole.owner> + test@example.com as MemberRole.owner> >>> mlist.administrators.get_member('aperson@example.com') <Member: Anne Person <aperson@example.com> on - _xtest@example.com as MemberRole.owner> + test@example.com as MemberRole.owner> >>> mlist.members.get_member('aperson@example.com') <Member: Anne Person <aperson@example.com> on - _xtest@example.com as MemberRole.member> + test@example.com as MemberRole.member> + >>> mlist.nonmembers.get_member('fperson@example.com') + <Member: Fred <fperson@example.com> on + test@example.com as MemberRole.nonmember> However, if the address is not subscribed with the appropriate role, then None is returned. @@ -202,6 +264,8 @@ is returned. None >>> print mlist.members.get_member('zperson@example.com') None + >>> print mlist.nonmembers.get_member('aperson@example.com') + None All subscribers @@ -212,13 +276,14 @@ regardless of their role. >>> def sortkey(member): ... return (member.address.address, int(member.role)) - >>> [(member.address.address, str(member.role)) - ... for member in sorted(mlist.subscribers.members, key=sortkey)] - [(u'aperson@example.com', 'MemberRole.member'), - (u'aperson@example.com', 'MemberRole.owner'), - (u'bperson@example.com', 'MemberRole.member'), - (u'bperson@example.com', 'MemberRole.moderator'), - (u'cperson@example.com', 'MemberRole.member')] + >>> for member in sorted(mlist.subscribers.members, key=sortkey): + ... print member.address.address, member.role + aperson@example.com MemberRole.member + aperson@example.com MemberRole.owner + bperson@example.com MemberRole.member + bperson@example.com MemberRole.moderator + cperson@example.com MemberRole.member + fperson@example.com MemberRole.nonmember Double subscriptions @@ -230,28 +295,35 @@ It is an error to subscribe someone to a list with the same role twice. Traceback (most recent call last): ... AlreadySubscribedError: aperson@example.com is already a MemberRole.owner - of mailing list _xtest@example.com + of mailing list test@example.com -Moderation flag -=============== +Moderation actions +================== -All members have a moderation flag which specifies whether postings from that -member must first be approved by the list owner or moderator. The default -value of this flag is inherited from the mailing lists configuration. -:: +All members of any role have a *moderation action* which specifies how +postings from that member are handled. By default, owners and moderators are +automatically accepted for posting to the mailing list. + + >>> from operator import attrgetter + >>> for member in sorted(mlist.administrators.members, + ... key=attrgetter('address.address')): + ... print member.address.address, member.role, member.moderation_action + aperson@example.com MemberRole.owner Action.accept + bperson@example.com MemberRole.moderator Action.accept + +By default, members have a *deferred* action which specifies that the posting +should go through the normal moderation checks. + + >>> for member in sorted(mlist.members.members, + ... key=attrgetter('address.address')): + ... print member.address.address, member.role, member.moderation_action + aperson@example.com MemberRole.member Action.defer + bperson@example.com MemberRole.member Action.defer + cperson@example.com MemberRole.member Action.defer - >>> mlist.default_member_moderation - False - >>> user_4 = user_manager.create_user('dperson@example.com', 'Dave') - >>> address_4 = list(user_4.addresses)[0] - >>> member_4 = address_4.subscribe(mlist, MemberRole.member) - >>> member_4.is_moderated - False +Postings by nonmembers are held for moderator approval by default. - >>> mlist.default_member_moderation = True - >>> user_5 = user_manager.create_user('eperson@example.com', 'Elly') - >>> address_5 = list(user_5.addresses)[0] - >>> member_5 = address_5.subscribe(mlist, MemberRole.member) - >>> member_5.is_moderated - True + >>> for member in mlist.nonmembers.members: + ... print member.address.address, member.role, member.moderation_action + fperson@example.com MemberRole.nonmember Action.hold diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 19116af6b..a4a1ba1b0 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -119,7 +119,8 @@ class MailingList(Model): bounce_unrecognized_goes_to_list_owner = Bool() # XXX bounce_you_are_disabled_warnings = Int() # XXX bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX - default_member_moderation = Bool() + default_member_action = Enum() + default_nonmember_action = Enum() description = Unicode() digest_footer = Unicode() digest_header = Unicode() @@ -144,7 +145,6 @@ class MailingList(Model): max_days_to_hold = Int() max_message_size = Int() max_num_recipients = Int() - member_moderation_action = Enum() member_moderation_notice = Unicode() mime_is_default_digest = Bool() moderator_password = Unicode() @@ -205,6 +205,7 @@ class MailingList(Model): self.regular_members = roster.RegularMemberRoster(self) self.digest_members = roster.DigestMemberRoster(self) self.subscribers = roster.Subscribers(self) + self.nonmembers = roster.NonmemberRoster(self) def __repr__(self): return '<mailing list "{0}" at {1:#x}>'.format( diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index 48cd54b28..5dde5c629 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -24,7 +24,7 @@ __all__ = [ 'Member', ] -from storm.locals import Bool, Int, Reference, Unicode +from storm.locals import Int, Reference, Unicode from zope.component import getUtility from zope.interface import implements @@ -32,8 +32,9 @@ from mailman.config import config from mailman.core.constants import system_preferences from mailman.database.model import Model from mailman.database.types import Enum +from mailman.interfaces.action import Action from mailman.interfaces.listmanager import IListManager -from mailman.interfaces.member import IMember +from mailman.interfaces.member import IMember, MemberRole @@ -43,7 +44,7 @@ class Member(Model): id = Int(primary=True) role = Enum() mailing_list = Unicode() - is_moderated = Bool() + moderation_action = Enum() address_id = Int() address = Reference(address_id, 'Address.id') @@ -54,8 +55,16 @@ class Member(Model): self.role = role self.mailing_list = mailing_list self.address = address - self.is_moderated = getUtility(IListManager).get( - mailing_list).default_member_moderation + if role in (MemberRole.owner, MemberRole.moderator): + self.moderation_action = Action.accept + elif role is MemberRole.member: + self.moderation_action = getUtility(IListManager).get( + mailing_list).default_member_action + else: + assert role is MemberRole.nonmember, ( + 'Invalid MemberRole: {0}'.format(role)) + self.moderation_action = getUtility(IListManager).get( + mailing_list).default_nonmember_action def __repr__(self): return '<Member: {0} on {1} as {2}>'.format( diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py index bffafd809..89ef98531 100644 --- a/src/mailman/model/roster.py +++ b/src/mailman/model/roster.py @@ -120,6 +120,14 @@ class MemberRoster(AbstractRoster): +class NonmemberRoster(AbstractRoster): + """Return all the nonmembers of a list.""" + + name = 'nonmember' + role = MemberRole.nonmember + + + class OwnerRoster(AbstractRoster): """Return all the owners of a list.""" diff --git a/src/mailman/pipeline/moderate.py b/src/mailman/pipeline/moderate.py deleted file mode 100644 index 2a59592e5..000000000 --- a/src/mailman/pipeline/moderate.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (C) 2001-2010 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Posting moderation filter.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'process', - ] - - -import re - -from email.MIMEMessage import MIMEMessage -from email.MIMEText import MIMEText - -from mailman.Utils import wrap -from mailman.config import config -from mailman.core import errors -from mailman.core.i18n import _ -from mailman.email.message import UserNotification - - - -## class ModeratedMemberPost(Hold.ModeratedPost): -## # BAW: I wanted to use the reason below to differentiate between this -## # situation and normal ModeratedPost reasons. Greg Ward and Stonewall -## # Ballard thought the language was too harsh and mentioned offense taken -## # by some list members. I'd still like this class's reason to be -## # different than the base class's reason, but we'll use this until -## # someone can come up with something more clever but inoffensive. -## # -## # reason = _('Posts by member are currently quarantined for moderation') -## pass - - - -def process(mlist, msg, msgdata): - if msgdata.get('approved') or msgdata.get('fromusenet'): - return - # First of all, is the poster a member or not? - for sender in msg.senders: - if mlist.isMember(sender): - break - else: - sender = None - if sender: - # If the member's moderation flag is on, then perform the moderation - # action. - if mlist.getMemberOption(sender, config.Moderate): - # Note that for member_moderation_action, 0==Hold, 1=Reject, - # 2==Discard - if mlist.member_moderation_action == 0: - # Hold. BAW: WIBNI we could add the member_moderation_notice - # to the notice sent back to the sender? - msgdata['sender'] = sender - Hold.hold_for_approval(mlist, msg, msgdata, - ModeratedMemberPost) - elif mlist.member_moderation_action == 1: - # Reject - text = mlist.member_moderation_notice - if text: - text = wrap(text) - else: - # Use the default RejectMessage notice string - text = None - raise errors.RejectMessage, text - elif mlist.member_moderation_action == 2: - # Discard. BAW: Again, it would be nice if we could send a - # discard notice to the sender - raise errors.DiscardMessage - else: - assert 0, 'bad member_moderation_action' - # Should we do anything explict to mark this message as getting past - # this point? No, because further pipeline handlers will need to do - # their own thing. - return - else: - sender = msg.sender - # From here on out, we're dealing with non-members. - if matches_p(sender, mlist.accept_these_nonmembers): - return - if matches_p(sender, mlist.hold_these_nonmembers): - Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost) - # No return - if matches_p(sender, mlist.reject_these_nonmembers): - do_reject(mlist) - # No return - if matches_p(sender, mlist.discard_these_nonmembers): - do_discard(mlist, msg) - # No return - # Okay, so the sender wasn't specified explicitly by any of the non-member - # moderation configuration variables. Handle by way of generic non-member - # action. - assert 0 <= mlist.generic_nonmember_action <= 4 - if mlist.generic_nonmember_action == 0: - # Accept - return - elif mlist.generic_nonmember_action == 1: - Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost) - elif mlist.generic_nonmember_action == 2: - do_reject(mlist) - elif mlist.generic_nonmember_action == 3: - do_discard(mlist, msg) - - - -def matches_p(sender, nonmembers): - # First strip out all the regular expressions. - addresses = set(address.lower() for address in nonmembers - if not address.startswith('^')) - if sender in addresses: - return True - # Now do the regular expression matches. - for regexp in nonmembers: - if regexp.startswith('^'): - try: - cre = re.compile(regexp, re.IGNORECASE) - except re.error: - continue - if cre.search(sender): - return True - return False - - - -def do_reject(mlist): - listowner = mlist.GetOwnerEmail() - if mlist.nonmember_rejection_notice: - raise errors.RejectMessage, \ - wrap(_(mlist.nonmember_rejection_notice)) - else: - raise errors.RejectMessage, wrap(_("""\ -You are not allowed to post to this mailing list, and your message has been -automatically rejected. If you think that your messages are being rejected in -error, contact the mailing list owner at ${listowner}.""")) - - - -def do_discard(mlist, msg): - # Do we forward auto-discards to the list owners? - if mlist.forward_auto_discards: - varhelp = '%s/?VARHELP=privacy/sender/discard_these_nonmembers' % \ - mlist.GetScriptURL('admin', absolute=1) - nmsg = UserNotification(mlist.GetOwnerEmail(), - mlist.GetBouncesEmail(), - _('Auto-discard notification'), - lang=mlist.preferred_language) - nmsg.set_type('multipart/mixed') - text = MIMEText(wrap(_( - 'The attached message has been automatically discarded.')), - _charset=mlist.preferred_language.charset) - nmsg.attach(text) - nmsg.attach(MIMEMessage(msg)) - nmsg.send(mlist) - # Discard this sucker - raise errors.DiscardMessage diff --git a/src/mailman/queue/docs/incoming.txt b/src/mailman/queue/docs/incoming.txt index 24e7ecfd1..fd875105c 100644 --- a/src/mailman/queue/docs/incoming.txt +++ b/src/mailman/queue/docs/incoming.txt @@ -62,9 +62,9 @@ And now the message is in the pipeline queue. Subject: My first post Message-ID: <first> Date: ... - X-Mailman-Rule-Misses: approved; emergency; loop; administrivia; - implicit-dest; max-recipients; max-size; news-moderation; no-subject; - suspicious-header; member-moderation + X-Mailman-Rule-Misses: approved; emergency; loop; moderation; + administrivia; implicit-dest; max-recipients; max-size; + news-moderation; no-subject; suspicious-header <BLANKLINE> First post! <BLANKLINE> diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py index 7b7c127c2..d6982b142 100644 --- a/src/mailman/rest/configuration.py +++ b/src/mailman/rest/configuration.py @@ -174,7 +174,8 @@ ATTRIBUTES = dict( collapse_alternatives=GetterSetter(as_boolean), convert_html_to_plaintext=GetterSetter(as_boolean), created_at=GetterSetter(None), - default_member_moderation=GetterSetter(as_boolean), + default_member_action=GetterSetter(enum_validator(Action)), + default_nonmember_action=GetterSetter(enum_validator(Action)), description=GetterSetter(unicode), digest_last_sent_at=GetterSetter(None), digest_size_threshold=GetterSetter(float), @@ -189,7 +190,6 @@ ATTRIBUTES = dict( leave_address=GetterSetter(None), list_id=GetterSetter(None), list_name=GetterSetter(None), - member_moderation_action=GetterSetter(enum_validator(Action)), next_digest_number=GetterSetter(None), no_reply_address=GetterSetter(None), owner_address=GetterSetter(None), diff --git a/src/mailman/rest/docs/configuration.txt b/src/mailman/rest/docs/configuration.txt index 2269357ca..b149a9431 100644 --- a/src/mailman/rest/docs/configuration.txt +++ b/src/mailman/rest/docs/configuration.txt @@ -32,7 +32,8 @@ All readable attributes for a list are available on a sub-resource. collapse_alternatives: True convert_html_to_plaintext: False created_at: 20...T... - default_member_moderation: False + default_member_action: defer + default_nonmember_action: hold description: digest_last_sent_at: None digest_size_threshold: 30.0 @@ -48,7 +49,6 @@ All readable attributes for a list are available on a sub-resource. leave_address: test-one-leave@example.com list_id: test-one.example.com list_name: test-one - member_moderation_action: hold next_digest_number: 1 no_reply_address: noreply@example.com owner_address: test-one-owner@example.com @@ -72,6 +72,7 @@ Not all of the readable attributes can be set through the web interface. The ones that can, can either be set via ``PUT`` or ``PATCH``. ``PUT`` changes all the writable attributes in one request. + >>> from mailman.interfaces.action import Action >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( @@ -100,8 +101,8 @@ all the writable attributes in one request. ... reply_goes_to_list='point_to_list', ... send_welcome_msg=False, ... welcome_msg='Welcome!', - ... member_moderation_action='reject', - ... default_member_moderation=True, + ... default_member_action='hold', + ... default_nonmember_action='discard', ... generic_nonmember_action=2, ... ), ... 'PUT') @@ -131,7 +132,8 @@ These values are changed permanently. collapse_alternatives: False convert_html_to_plaintext: True ... - default_member_moderation: True + default_member_action: hold + default_nonmember_action: discard description: This is my mailing list ... digest_size_threshold: 10.5 @@ -140,8 +142,6 @@ These values are changed permanently. include_list_post_header: False include_rfc2369_headers: False ... - member_moderation_action: reject - ... pipeline: virgin ... real_name: Fnords @@ -182,8 +182,8 @@ must be included. It is an error to leave one or more out... ... reply_goes_to_list='point_to_list', ... send_welcome_msg=True, ... welcome_msg='welcome message', - ... member_moderation_action='reject', - ... default_member_moderation=True, + ... default_member_action='accept', + ... default_nonmember_action='accept', ... generic_nonmember_action=2, ... ), ... 'PUT') diff --git a/src/mailman/rules/docs/moderation.txt b/src/mailman/rules/docs/moderation.txt index 96ad8b6c3..ce88c8576 100644 --- a/src/mailman/rules/docs/moderation.txt +++ b/src/mailman/rules/docs/moderation.txt @@ -2,74 +2,95 @@ Member moderation ================= -Each user has a moderation flag. When set, and the list is set to moderate -postings, then only members with a cleared moderation flag will be able to -email the list without having those messages be held for approval. The -'moderation' rule determines whether the message should be moderated or not. +All members and nonmembers have a moderation action. When the action is not +`defer`, the `moderation` rule flags the message as needing a moderation +shortcut. This might be to automatically accept, discard, reject, or hold the +message. - >>> mlist = create_list('_xtest@example.com') - >>> rule = config.rules['member-moderation'] + >>> mlist = create_list('test@example.com') + >>> rule = config.rules['moderation'] >>> print rule.name - member-moderation - -In the simplest case, the sender is not a member of the mailing list, so the -moderation rule can't match. - - >>> msg = message_from_string("""\ - ... From: aperson@example.org - ... To: _xtest@example.com - ... Subject: A posted message - ... - ... """) - >>> rule.check(mlist, msg, {}) - False + moderation Let's add the message author as a non-moderated member. +:: + >>> from mailman.interfaces.member import MemberRole >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility >>> user = getUtility(IUserManager).create_user( ... 'aperson@example.org', 'Anne Person') -Because the member is not moderated, the rule does not match. - >>> address = list(user.addresses)[0] - >>> from mailman.interfaces.member import MemberRole >>> member = address.subscribe(mlist, MemberRole.member) - >>> member.is_moderated - False + >>> print member.moderation_action + Action.defer + +Because the member is not moderated, the rule does not match. + + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... To: test@example.com + ... Subject: A posted message + ... + ... """) >>> rule.check(mlist, msg, {}) False -Once the member's moderation flag is set though, the rule matches. +Once the member's moderation action is set to something other than `defer`, +the rule matches. Also, the message metadata has a few extra pieces of +information for the eventual moderation chain. - >>> member.is_moderated = True - >>> rule.check(mlist, msg, {}) + >>> from mailman.interfaces.action import Action + >>> member.moderation_action = Action.hold + >>> msgdata = {} + >>> rule.check(mlist, msg, msgdata) True + >>> dump_msgdata(msgdata) + moderation_action: hold + moderation_sender: aperson@example.org -Non-members -=========== +Nonmembers +========== -There is another, related rule for matching non-members, which simply matches -if the sender is *not* a member of the mailing list. +Nonmembers are handled in a similar way, although by default, nonmember +postings are held for moderator approval. - >>> rule = config.rules['non-member'] - >>> print rule.name - non-member - -If the sender is a member of this mailing list, the rule does not match. + >>> user = getUtility(IUserManager).create_user( + ... 'bperson@example.org', 'Bart Person') - >>> rule.check(mlist, msg, {}) - False + >>> address = list(user.addresses)[0] + >>> nonmember = address.subscribe(mlist, MemberRole.nonmember) + >>> print nonmember.moderation_action + Action.hold -But if the sender is not a member of this mailing list, the rule matches. +Because the sender's moderation action is to hold by default, the rule +matches. Again, the message metadata carries some useful information. >>> msg = message_from_string("""\ ... From: bperson@example.org - ... To: _xtest@example.com + ... To: test@example.com ... Subject: A posted message ... ... """) - >>> rule.check(mlist, msg, {}) + >>> msgdata = {} + >>> rule.check(mlist, msg, msgdata) True + >>> dump_msgdata(msgdata) + moderation_action: hold + moderation_sender: bperson@example.org + +Of course, the nonmember action can be set to defer the decision, in which +case the rule does not match. + + >>> nonmember.moderation_action = Action.defer + >>> rule.check(mlist, msg, {}) + False + + +Unregistered nonmembers +======================= + +XXX diff --git a/src/mailman/rules/docs/rules.txt b/src/mailman/rules/docs/rules.txt index 056e39cab..321f1b277 100644 --- a/src/mailman/rules/docs/rules.txt +++ b/src/mailman/rules/docs/rules.txt @@ -26,10 +26,9 @@ names to rule objects. loop True max-recipients True max-size True - member-moderation True + moderation True news-moderation True no-subject True - non-member True suspicious-header True truth True diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py index 4bf6ba1c8..efca78fd4 100644 --- a/src/mailman/rules/moderation.py +++ b/src/mailman/rules/moderation.py @@ -21,48 +21,49 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ - 'MemberModeration', - 'NonMemberModeration', + 'Moderation', ] from zope.interface import implements from mailman.core.i18n import _ +from mailman.interfaces.action import Action from mailman.interfaces.rules import IRule -class MemberModeration: +class Moderation: """The member moderation rule.""" implements(IRule) - name = 'member-moderation' - description = _('Match messages sent by moderated members.') + name = 'moderation' + description = _('Match messages sent by moderated members and nonmembers.') record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" for sender in msg.senders: member = mlist.members.get_member(sender) - if member is not None and member.is_moderated: + action = (Action.defer if member is None + else member.moderation_action) + if action is not Action.defer: + # We must stringify the moderation action so that it can be + # stored in the pending request table. + msgdata['moderation_action'] = action.enumname + msgdata['moderation_sender'] = sender return True - return False - - - -class NonMemberModeration: - """The non-membership rule.""" - implements(IRule) - - name = 'non-member' - description = _('Match messages sent by non-members.') - record = True - - def check(self, mlist, msg, msgdata): - """See `IRule`.""" for sender in msg.senders: - if mlist.members.get_member(sender) is not None: - # The sender is a member of the mailing list. - return False - return True + nonmember = mlist.nonmembers.get_member(sender) + action = (Action.defer if nonmember is None + else nonmember.moderation_action) + if action is not Action.defer: + # We must stringify the moderation action so that it can be + # stored in the pending request table. + msgdata['moderation_action'] = action.enumname + msgdata['moderation_sender'] = sender + return True + # XXX This is not correct. If the sender is neither a member nor a + # nonmember, we need to register them as a nonmember and give them the + # default action. + return False diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py index 907b5b7dc..167ff1d19 100644 --- a/src/mailman/styles/default.py +++ b/src/mailman/styles/default.py @@ -116,13 +116,13 @@ ${listinfo_page} mlist.nondigestable = True mlist.personalize = Personalization.none # New sender-centric moderation (privacy) options - mlist.default_member_moderation = False + mlist.default_member_action = Action.defer + mlist.default_nonmember_action = Action.hold # Archiver mlist.archive = True mlist.archive_private = 0 mlist.archive_volume_frequency = 1 mlist.emergency = False - mlist.member_moderation_action = Action.hold mlist.member_moderation_notice = '' mlist.accept_these_nonmembers = [] mlist.hold_these_nonmembers = [] diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py index 5362b7b45..19e14c1c0 100644 --- a/src/mailman/utilities/importer.py +++ b/src/mailman/utilities/importer.py @@ -47,7 +47,6 @@ TYPES = dict( bounce_info_stale_after=seconds_to_delta, bounce_you_are_disabled_warnings_interval=seconds_to_delta, digest_volume_frequency=DigestFrequency, - member_moderation_action=Action, news_moderation=NewsModeration, personalize=Personalization, reply_goes_to_list=ReplyToMunging, |
