Commit 34975c2d authored by Aurélien Bompard's avatar Aurélien Bompard

Resurrect Barry's subpolicy branch (lp:~barry/mailman/subpolicy)

parent 6280c5ff
......@@ -238,14 +238,16 @@ Holding subscription requests
For closed lists, subscription requests will also be held for moderator
approval. In this case, several pieces of information related to the
subscription must be provided, including the subscriber's address and real
name, their password (possibly hashed), what kind of delivery option they are
choosing and their preferred language.
name, what kind of delivery option they are choosing and their preferred
language.
>>> from mailman.app.moderator import hold_subscription
>>> from mailman.interfaces.member import DeliveryMode
>>> from mailman.interfaces.subscriptions import RequestRecord
>>> req_id = hold_subscription(
... mlist, 'fred@example.org', 'Fred Person',
... '{NONE}abcxyz', DeliveryMode.regular, 'en')
... mlist,
... RequestRecord('fred@example.org', 'Fred Person',
... DeliveryMode.regular, 'en'))
Disposing of membership change requests
......@@ -269,8 +271,9 @@ The held subscription can also be discarded.
Gwen tries to subscribe to the mailing list, but...
>>> req_id = hold_subscription(
... mlist, 'gwen@example.org', 'Gwen Person',
... '{NONE}zyxcba', DeliveryMode.regular, 'en')
... mlist,
... RequestRecord('gwen@example.org', 'Gwen Person',
... DeliveryMode.regular, 'en'))
...her request is rejected...
......@@ -305,8 +308,9 @@ mailing list.
>>> mlist.send_welcome_message = False
>>> req_id = hold_subscription(
... mlist, 'herb@example.org', 'Herb Person',
... 'abcxyz', DeliveryMode.regular, 'en')
... mlist,
... RequestRecord('herb@example.org', 'Herb Person',
... DeliveryMode.regular, 'en'))
The moderators accept the subscription request.
......@@ -399,8 +403,9 @@ list is configured to send them.
Iris tries to subscribe to the mailing list.
>>> req_id = hold_subscription(mlist, 'iris@example.org', 'Iris Person',
... 'password', DeliveryMode.regular, 'en')
>>> req_id = hold_subscription(mlist,
... RequestRecord('iris@example.org', 'Iris Person',
... DeliveryMode.regular, 'en'))
There's now a message in the virgin queue, destined for the list owner.
......@@ -491,8 +496,9 @@ can get a welcome message.
>>> mlist.admin_notify_mchanges = False
>>> mlist.send_welcome_message = True
>>> req_id = hold_subscription(mlist, 'kate@example.org', 'Kate Person',
... 'password', DeliveryMode.regular, 'en')
>>> req_id = hold_subscription(mlist,
... RequestRecord('kate@example.org', 'Kate Person',
... DeliveryMode.regular, 'en'))
>>> handle_subscription(mlist, req_id, Action.accept)
>>> messages = get_queue_messages('virgin')
>>> len(messages)
......
......@@ -40,8 +40,7 @@ from zope.component import getUtility
def add_member(mlist, email, display_name, password, delivery_mode, language,
role=MemberRole.member):
def add_member(mlist, record, role=MemberRole.member):
"""Add a member right now.
The member's subscription must be approved by whatever policy the list
......@@ -49,16 +48,8 @@ def add_member(mlist, email, display_name, password, delivery_mode, language,
:param mlist: The mailing list to add the member to.
:type mlist: `IMailingList`
:param email: The email address to subscribe.
:type email: str
:param display_name: The subscriber's full name.
:type display_name: str
:param password: The subscriber's plain text password.
:type password: str
:param delivery_mode: The delivery mode the subscriber has chosen.
:type delivery_mode: DeliveryMode
:param language: The language that the subscriber is going to use.
:type language: str
:param record: a subscription request record.
:type record: RequestRecord
:param role: The membership role for this subscription.
:type role: `MemberRole`
:return: The just created member.
......@@ -69,62 +60,74 @@ def add_member(mlist, email, display_name, password, delivery_mode, language,
:raises MembershipIsBannedError: if the membership is not allowed.
"""
# Check to see if the email address is banned.
if IBanManager(mlist).is_banned(email):
raise MembershipIsBannedError(mlist, email)
# See if there's already a user linked with the given address.
if IBanManager(mlist).is_banned(record.email):
raise MembershipIsBannedError(mlist, record.email)
# Make sure there is a user linked with the given address.
user_manager = getUtility(IUserManager)
user = user_manager.get_user(email)
if user is None:
# A user linked to this address does not yet exist. Is the address
# itself known but just not linked to a user?
address = user_manager.get_address(email)
if address is None:
# Nope, we don't even know about this address, so create both the
# user and address now.
user = user_manager.create_user(email, display_name)
# Do it this way so we don't have to flush the previous change.
address = list(user.addresses)[0]
else:
# The address object exists, but it's not linked to a user.
# Create the user and link it now.
user = user_manager.create_user()
user.display_name = (
display_name if display_name else address.display_name)
user.link(address)
# Encrypt the password using the currently selected hash scheme.
user.password = config.password_context.encrypt(password)
user.preferences.preferred_language = language
member = mlist.subscribe(address, role)
member.preferences.delivery_mode = delivery_mode
else:
# The user exists and is linked to the case-insensitive address.
# We're looking for two versions of the email address, the case
# preserved version and the case insensitive version. We'll
# subscribe the version with matching case if it exists, otherwise
# we'll use one of the matching case-insensitively ones. It's
# undefined which one we pick.
case_preserved = None
case_insensitive = None
for address in user.addresses:
if address.original_email == email:
case_preserved = address
if address.email == email.lower():
case_insensitive = address
assert case_preserved is not None or case_insensitive is not None, (
'Could not find a linked address for: {}'.format(email))
address = (case_preserved if case_preserved is not None
else case_insensitive)
# Create the member and set the appropriate preferences. It's
# possible we're subscribing the lower cased version of the address;
# if that's already subscribed re-issue the exception with the correct
# email address (i.e. the one passed in here).
try:
member = mlist.subscribe(address, role)
except AlreadySubscribedError as error:
raise AlreadySubscribedError(
error.fqdn_listname, email, error.role)
member.preferences.preferred_language = language
member.preferences.delivery_mode = delivery_mode
user = user_manager.make_user(record.email, record.display_name)
# Encrypt the password using the currently selected hash scheme.
user.preferences.preferred_language = record.language
# Subscribe the address, not the user.
address = user_manager.get_address(record.email)
if address is None or address.user is not user:
raise AssertionError(
'User should have had linked address: {0}'.format(address))
# Create the member and set the appropriate preferences.
member = mlist.subscribe(address, role)
member.preferences.preferred_language = record.language
member.preferences.delivery_mode = record.delivery_mode
# user = user_manager.get_user(email)
# if user is None:
# # A user linked to this address does not yet exist. Is the address
# # itself known but just not linked to a user?
# address = user_manager.get_address(email)
# if address is None:
# # Nope, we don't even know about this address, so create both the
# # user and address now.
# user = user_manager.create_user(email, display_name)
# # Do it this way so we don't have to flush the previous change.
# address = list(user.addresses)[0]
# else:
# # The address object exists, but it's not linked to a user.
# # Create the user and link it now.
# user = user_manager.create_user()
# user.display_name = (
# display_name if display_name else address.display_name)
# user.link(address)
# # Encrypt the password using the currently selected hash scheme.
# user.password = config.password_context.encrypt(password)
# user.preferences.preferred_language = language
# member = mlist.subscribe(address, role)
# member.preferences.delivery_mode = delivery_mode
# else:
# # The user exists and is linked to the case-insensitive address.
# # We're looking for two versions of the email address, the case
# # preserved version and the case insensitive version. We'll
# # subscribe the version with matching case if it exists, otherwise
# # we'll use one of the matching case-insensitively ones. It's
# # undefined which one we pick.
# case_preserved = None
# case_insensitive = None
# for address in user.addresses:
# if address.original_email == email:
# case_preserved = address
# if address.email == email.lower():
# case_insensitive = address
# assert case_preserved is not None or case_insensitive is not None, (
# 'Could not find a linked address for: {}'.format(email))
# address = (case_preserved if case_preserved is not None
# else case_insensitive)
# # Create the member and set the appropriate preferences. It's
# # possible we're subscribing the lower cased version of the address;
# # if that's already subscribed re-issue the exception with the correct
# # email address (i.e. the one passed in here).
# try:
# member = mlist.subscribe(address, role)
# except AlreadySubscribedError as error:
# raise AlreadySubscribedError(
# error.fqdn_listname, email, error.role)
# member.preferences.preferred_language = language
# member.preferences.delivery_mode = delivery_mode
return member
......
......@@ -44,6 +44,7 @@ from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, NotAMemberError)
from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.requests import IListRequests, RequestType
from mailman.interfaces.subscriptions import RequestRecord
from mailman.utilities.datetime import now
from mailman.utilities.i18n import make
from zope.component import getUtility
......@@ -192,26 +193,26 @@ def handle_message(mlist, id, action,
def hold_subscription(mlist, address, display_name, password, mode, language):
def hold_subscription(mlist, record):
data = dict(when=now().isoformat(),
address=address,
display_name=display_name,
password=password,
delivery_mode=mode.name,
language=language)
# Now hold this request. We'll use the address as the key.
email=record.email,
display_name=record.display_name,
delivery_mode=record.delivery_mode.name,
language=record.language)
# Now hold this request. We'll use the email address as the key.
requestsdb = IListRequests(mlist)
request_id = requestsdb.hold_request(
RequestType.subscription, address, data)
RequestType.subscription, record.email, data)
vlog.info('%s: held subscription request from %s',
mlist.fqdn_listname, address)
mlist.fqdn_listname, record.email)
# Possibly notify the administrator in default list language
if mlist.admin_immed_notify:
email = record.email # XXX: seems unnecessary
subject = _(
'New subscription request to $mlist.display_name from $address')
'New subscription request to $mlist.display_name from $email')
text = make('subauth.txt',
mailing_list=mlist,
username=address,
username=record.email,
listname=mlist.fqdn_listname,
admindb_url=mlist.script_url('admindb'),
)
......@@ -236,19 +237,19 @@ def handle_subscription(mlist, id, action, comment=None):
elif action is Action.reject:
key, data = requestdb.get_request(id)
_refuse(mlist, _('Subscription request'),
data['address'],
data['email'],
comment or _('[No reason given]'),
lang=getUtility(ILanguageManager)[data['language']])
elif action is Action.accept:
key, data = requestdb.get_request(id)
delivery_mode = DeliveryMode[data['delivery_mode']]
address = data['address']
email = data['email']
display_name = data['display_name']
language = getUtility(ILanguageManager)[data['language']]
password = data['password']
try:
add_member(mlist, address, display_name, password,
delivery_mode, language)
add_member(
mlist,
RequestRecord(email, display_name, delivery_mode, language))
except AlreadySubscribedError:
# The address got subscribed in some other way after the original
# request was made and accepted.
......@@ -256,9 +257,9 @@ def handle_subscription(mlist, id, action, comment=None):
else:
if mlist.admin_notify_mchanges:
send_admin_subscription_notice(
mlist, address, display_name, language)
mlist, email, display_name, language)
slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
delivery_mode, formataddr((display_name, address)),
delivery_mode, formataddr((display_name, email)),
'via admin approval')
else:
raise AssertionError('Unexpected action: {0}'.format(action))
......@@ -267,20 +268,20 @@ def handle_subscription(mlist, id, action, comment=None):
def hold_unsubscription(mlist, address):
data = dict(address=address)
def hold_unsubscription(mlist, email):
data = dict(email=email)
requestsdb = IListRequests(mlist)
request_id = requestsdb.hold_request(
RequestType.unsubscription, address, data)
RequestType.unsubscription, email, data)
vlog.info('%s: held unsubscription request from %s',
mlist.fqdn_listname, address)
mlist.fqdn_listname, email)
# Possibly notify the administrator of the hold
if mlist.admin_immed_notify:
subject = _(
'New unsubscription request from $mlist.display_name by $address')
'New unsubscription request from $mlist.display_name by $email')
text = make('unsubauth.txt',
mailing_list=mlist,
address=address,
email=email,
listname=mlist.fqdn_listname,
admindb_url=mlist.script_url('admindb'),
)
......@@ -297,7 +298,7 @@ def hold_unsubscription(mlist, address):
def handle_unsubscription(mlist, id, action, comment=None):
requestdb = IListRequests(mlist)
key, data = requestdb.get_request(id)
address = data['address']
email = data['email']
if action is Action.defer:
# Nothing to do.
return
......@@ -306,16 +307,16 @@ def handle_unsubscription(mlist, id, action, comment=None):
pass
elif action is Action.reject:
key, data = requestdb.get_request(id)
_refuse(mlist, _('Unsubscription request'), address,
_refuse(mlist, _('Unsubscription request'), email,
comment or _('[No reason given]'))
elif action is Action.accept:
key, data = requestdb.get_request(id)
try:
delete_member(mlist, address)
delete_member(mlist, email)
except NotAMemberError:
# User has already been unsubscribed.
pass
slog.info('%s: deleted %s', mlist.fqdn_listname, address)
slog.info('%s: deleted %s', mlist.fqdn_listname, email)
else:
raise AssertionError('Unexpected action: {0}'.format(action))
# Delete the request from the database.
......
......@@ -19,28 +19,35 @@
__all__ = [
'SubscriptionService',
'SubscriptionWorkflow',
'handle_ListDeletingEvent',
]
from collections import deque
from operator import attrgetter
from passlib.utils import generate_password as generate
#from passlib.utils import generate_password as generate
from sqlalchemy import and_, or_
from uuid import UUID
from zope.component import getUtility
from zope.interface import implementer
from mailman.app.membership import add_member, delete_member
from mailman.config import config
#from mailman.config import config
from mailman.app.moderator import hold_subscription
from mailman.core.constants import system_preferences
from mailman.database.transaction import dbconnection
from mailman.interfaces.address import IAddress
from mailman.interfaces.listmanager import (
IListManager, ListDeletingEvent, NoSuchListError)
from mailman.interfaces.mailinglist import SubscriptionPolicy
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.subscriptions import (
ISubscriptionService, MissingUserError)
ISubscriptionService, MissingUserError, RequestRecord)
from mailman.interfaces.user import IUser
from mailman.interfaces.usermanager import IUserManager
from mailman.model.member import Member
from mailman.utilities.datetime import now
......@@ -53,6 +60,118 @@ def _membership_sort_key(member):
return (member.list_id, member.address.email, member.role.value)
class SubscriptionWorkflow:
"""Workflow of a subscription request."""
def __init__(self, mlist, subscriber,
pre_verified, pre_confirmed, pre_approved):
self.mlist = mlist
# The subscriber must be either an IUser or IAddress.
if IAddress.providedBy(subscriber):
self.address = subscriber
self.user = self.address.user
elif IUser.providedBy(subscriber):
self.address = subscriber.preferred_address
self.user = subscriber
self.subscriber = subscriber
self.pre_verified = pre_verified
self.pre_confirmed = pre_confirmed
self.pre_approved = pre_approved
# Prepare the state machine.
self._next = deque()
self._next.append(self._verification_check)
def __iter__(self):
return self
def _pop(self):
step = self._next.popleft()
# step could be a partial or a method.
name = getattr(step, 'func', step).__name__
return step, name
def __next__(self):
try:
step, name = self._pop()
step()
except IndexError:
raise StopIteration
except:
raise
def _maybe_set_preferred_address(self):
if self.user is None:
# The address has no linked user so create one, link it, and set
# the user's preferred address.
assert self.address is not None, 'No address or user'
self.user = getUtility(IUserManager).make_user(self.address.email)
self.user.preferred_address = self.address
elif self.user.preferred_address is None:
assert self.address is not None, 'No address or user'
# The address has a linked user, but no preferred address is set
# yet. This is required, so use the address.
self.user.preferred_address = self.address
def _verification_check(self):
if self.address.verified_on is not None:
# The address is already verified. Give the user a preferred
# address if it doesn't already have one. We may still have to do
# a subscription confirmation check. See below.
self._maybe_set_preferred_address()
else:
# The address is not yet verified. Maybe we're pre-verifying it.
# If so, we also want to give the user a preferred address if it
# doesn't already have one. We may still have to do a
# subscription confirmation check. See below.
if self.pre_verified:
self.address.verified_on = now()
self._maybe_set_preferred_address()
else:
# Since the address was not already verified, and not
# pre-verified, we have to send a confirmation check, which
# doubles as a verification step. Skip to that now.
self._next.append(self._send_confirmation)
return
self._next.append(self._confirmation_check)
def _confirmation_check(self):
# Must the user confirm their subscription request? If the policy is
# open subscriptions, then we need neither confirmation nor moderator
# approval, so just subscribe them now.
if self.mlist.subscription_policy == SubscriptionPolicy.open:
self._next.append(self._do_subscription)
elif self.pre_confirmed:
# No confirmation is necessary. We can skip to seeing whether a
# moderator confirmation is necessary.
self._next.append(self._moderation_check)
else:
self._next.append(self._send_confirmation)
def _moderation_check(self):
# Does the moderator need to approve the subscription request?
if self.mlist.subscription_policy in (
SubscriptionPolicy.moderate,
SubscriptionPolicy.confirm_then_moderate):
self._next.append(self._get_moderator_approval)
else:
# The moderator does not need to approve the subscription, so go
# ahead and do that now.
self._next.append(self._do_subscription)
def _get_moderator_approval(self):
# In order to get the moderator's approval, we need to hold the
# subscription request in the database
request = RequestRecord(
self.address.email, self.subscriber.display_name,
DeliveryMode.regular, 'en')
hold_subscription(self._mlist, request)
def _do_subscription(self):
# We can immediately subscribe the user to the mailing list.
self.mlist.subscribe(self.subscriber)
@implementer(ISubscriptionService)
class SubscriptionService:
......@@ -148,16 +267,11 @@ class SubscriptionService:
if isinstance(subscriber, str):
if display_name is None:
display_name, at, domain = subscriber.partition('@')
# Because we want to keep the REST API simple, there is no
# password or language given to us. We'll use the system's
# default language for the user's default language. We'll set the
# password to a system default. This will have to get reset since
# it can't be retrieved. Note that none of these are used unless
# the address is completely new to us.
password = generate(int(config.passwords.password_length))
return add_member(mlist, subscriber, display_name, password,
delivery_mode,
system_preferences.preferred_language, role)
return add_member(
mlist,
RequestRecord(subscriber, display_name, delivery_mode,
system_preferences.preferred_language),
role)
else:
# We have to assume it's a UUID.
assert isinstance(subscriber, UUID), 'Not a UUID'
......
......@@ -36,15 +36,15 @@ import unittest
from mailman.app.bounces import (
ProbeVERP, StandardVERP, bounce_message, maybe_forward, send_probe)
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member
from mailman.config import config
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.member import MemberRole
from mailman.interfaces.pending import IPendings
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
LogFileMark, get_queue_messages, specialized_message_from_string as mfs)
LogFileMark, get_queue_messages, specialized_message_from_string as mfs,
subscribe_ex)
from mailman.testing.layers import ConfigLayer
from zope.component import getUtility
......@@ -193,9 +193,8 @@ class TestSendProbe(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
self._mlist.send_welcome_message = False
self._member = add_member(self._mlist, 'anne@example.com',
'Anne Person', 'xxx',
DeliveryMode.regular, 'en')
self._member = subscribe_ex(
self._mlist, 'Anne', email='anne@example.com')
self._msg = mfs("""\
From: bouncer@example.com
To: anne@example.com
......@@ -285,9 +284,8 @@ class TestSendProbeNonEnglish(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
self._member = add_member(self._mlist, 'anne@example.com',
'Anne Person', 'xxx',
DeliveryMode.regular, 'en')
self._member = subscribe_ex(
self._mlist, 'Anne', email='anne@example.com')
self._msg = mfs("""\
From: bouncer@example.com
To: anne@example.com
......@@ -351,9 +349,8 @@ class TestProbe(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
self._mlist.send_welcome_message = False
self._member = add_member(self._mlist, 'anne@example.com',
'Anne Person', 'xxx',
DeliveryMode.regular, 'en')
self._member = subscribe_ex(
self._mlist, 'Anne', email='anne@example.com')
self._msg = mfs("""\
From: bouncer@example.com
To: anne@example.com
......
This diff is collapsed.
......@@ -19,16 +19,21 @@
__all__ = [
'TestModeration',
'TestUnsubscription',
]
import unittest
from mailman.app.lifecycle import create_list
from mailman.app.moderator import handle_message, hold_message
from mailman.app.moderator import (
handle_message, handle_subscription, handle_unsubscription, hold_message,
hold_subscription, hold_unsubscription)
from mailman.interfaces.action import Action
from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.requests import IListRequests
from mailman.interfaces.subscriptions import RequestRecord
from mailman.runners.incoming import IncomingRunner
from mailman.runners.outgoing import OutgoingRunner
from mailman.runners.pipeline import PipelineRunner
......@@ -148,3 +153,26 @@ Message-ID: <alpha>
'Forward of moderated message')
self.assertEqual(messages[0].msgdata['recipients'],
['zack@example.com'])
class TestUnsubscription(unittest.TestCase):
"""Test unsubscription requests."""
layer = SMTPLayer
def setUp(self):
self._mlist = create_list('test@example.com')
self._request_db = IListRequests(self._mlist)
def test_unsubscribe_defer(self):
# When unsubscriptions must be approved by the moderator, but the
# moderator defers this decision.
token = hold_subscription(
self._mlist,
RequestRecord('anne@example.org', 'Anne Person',
DeliveryMode.regular, 'en'))
handle_subscription(self._mlist, token, Action.accept)
# Now hold and handle an unsubscription request.
token = hold_unsubscription(self._mlist, 'anne@example.org')
handle_unsubscription(self._mlist, token, Action.defer)
......@@ -28,11 +28,11 @@ import tempfile
import unittest
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.testing.helpers import get_queue_messages
from mailman.interfaces.member import MemberRole
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import get_queue_messages, subscribe, subscribe_ex
from mailman.testing.layers import ConfigLayer
from zope.component import getUtility
......@@ -42,6 +42,7 @@ class TestNotifications(unittest.TestCase):
"""Test notifications."""
layer = ConfigLayer
maxDiff = None
def setUp(self):
self._mlist = create_list('test@example.com')
......@@ -78,8 +79,7 @@ Welcome to the $list_name mailing list.
shutil.rmtree(self.var_dir)
def test_welcome_message(self):
add_member(self._mlist, 'anne@example.com', 'Anne Person',
'password', DeliveryMode.regular, 'en')
subscribe(self._mlist, 'Anne', email='anne@example.com')
# Now there's one message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
......@@ -104,8 +104,12 @@ Welcome to the Test List mailing list.
# Add the xx language and subscribe Anne using it.
manager = getUtility(ILanguageManager)
manager.add('xx', 'us-ascii', 'Xlandia')
add_member(self._mlist, 'anne@example.com', 'Anne Person',
'password', DeliveryMode.regular, 'xx')
# We can't use the subscribe_ex() helper because that would send the
# welcome message before we set the member's preferred language.
address = getUtility(IUserManager).create_address(
'anne@example.com', 'Anne Person')
address.preferences.preferred_language = 'xx'
self._mlist.subscribe(address)
# Now there's one message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
......@@ -118,27 +122,29 @@ Welcome to the Test List mailing list.
def test_no_welcome_message_to_owners(self):
# Welcome messages go only to mailing list members, not to owners.
add_member(self._mlist, 'anne@example.com', 'Anne Person',
'password', DeliveryMode.regular, 'xx',
MemberRole.owner)
member = subscribe_ex(
self._mlist, 'Anne', MemberRole.owner, email='anne@example.com')
member.preferences.preferred_language = 'xx'
# There is no welcome message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 0)
def test_no_welcome_message_to_nonmembers(self):
# Welcome messages go only to mailing list members, not to nonmembers.
add_member(self._mlist, 'anne@example.com', 'Anne Person',