summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2007-06-18 10:50:23 -0400
committerBarry Warsaw2007-06-18 10:50:23 -0400
commit511a33778c4195c4abca7c58aa6917e6a77059b6 (patch)
tree5bffc7a5793a8247310294ac86931741e73b1fc5
parentf0e3b3934d5d458cadd814eeae07277b58650180 (diff)
downloadmailman-511a33778c4195c4abca7c58aa6917e6a77059b6.tar.gz
mailman-511a33778c4195c4abca7c58aa6917e6a77059b6.tar.zst
mailman-511a33778c4195c4abca7c58aa6917e6a77059b6.zip
-rw-r--r--Mailman/Handlers/Acknowledge.py22
-rw-r--r--Mailman/Utils.py2
-rw-r--r--Mailman/database/model/mailinglist.py12
-rw-r--r--Mailman/database/model/member.py35
-rw-r--r--Mailman/database/model/roster.py96
-rw-r--r--Mailman/docs/acknowledge.txt176
-rw-r--r--Mailman/docs/membership.txt67
-rw-r--r--Mailman/interfaces/member.py57
-rw-r--r--Mailman/interfaces/mlistweb.py25
-rw-r--r--Mailman/interfaces/roster.py7
-rw-r--r--Mailman/interfaces/rosterset.py47
-rw-r--r--Mailman/testing/test_acknowledge.py (renamed from Mailman/database/model/rosterset.py)33
-rw-r--r--Mailman/testing/test_handlers.py129
13 files changed, 425 insertions, 283 deletions
diff --git a/Mailman/Handlers/Acknowledge.py b/Mailman/Handlers/Acknowledge.py
index d02920cde..078c3ac92 100644
--- a/Mailman/Handlers/Acknowledge.py
+++ b/Mailman/Handlers/Acknowledge.py
@@ -36,30 +36,30 @@ __i18n_templates__ = True
def process(mlist, msg, msgdata):
# Extract the sender's address and find them in the user database
sender = msgdata.get('original_sender', msg.get_sender())
- try:
- ack = mlist.getMemberOption(sender, config.AcknowledgePosts)
- if not ack:
- return
- except Errors.NotAMemberError:
+ member = mlist.members.get_member(sender)
+ if member is None:
+ return
+ ack = member.acknowledge_posts
+ if not ack:
return
# Okay, they want acknowledgement of their post. Give them their original
# subject. BAW: do we want to use the decoded header?
origsubj = msgdata.get('origsubj', msg.get('subject', _('(no subject)')))
# Get the user's preferred language
- lang = msgdata.get('lang', mlist.getMemberLanguage(sender))
+ lang = msgdata.get('lang', member.preferred_language)
# Now get the acknowledgement template
realname = mlist.real_name
text = Utils.maketext(
'postack.txt',
{'subject' : Utils.oneline(origsubj, Utils.GetCharSet(lang)),
'listname' : realname,
- 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1),
- 'optionsurl' : mlist.GetOptionsURL(sender, absolute=1),
- }, lang=lang, mlist=mlist, raw=1)
+ 'listinfo_url': mlist.script_url('listinfo'),
+ 'optionsurl' : member.options_url,
+ }, lang=lang, mlist=mlist, raw=True)
# Craft the outgoing message, with all headers and attributes
# necessary for general delivery. Then enqueue it to the outgoing
# queue.
- subject = _('$realname post acknowledgement')
- usermsg = Message.UserNotification(sender, mlist.GetBouncesEmail(),
+ subject = _('$realname post acknowledgment')
+ usermsg = Message.UserNotification(sender, mlist.bounces_address,
subject, text, lang)
usermsg.send(mlist)
diff --git a/Mailman/Utils.py b/Mailman/Utils.py
index 27a61567e..064de570e 100644
--- a/Mailman/Utils.py
+++ b/Mailman/Utils.py
@@ -475,7 +475,7 @@ def findtext(templatefile, dict=None, raw=False, lang=None, mlist=None):
# Calculate the locations to scan
searchdirs = []
if mlist is not None:
- searchdirs.append(mlist.fullpath())
+ searchdirs.append(mlist.full_path)
searchdirs.append(os.path.join(config.TEMPLATE_DIR, mlist.host_name))
searchdirs.append(os.path.join(config.TEMPLATE_DIR, 'site'))
searchdirs.append(config.TEMPLATE_DIR)
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
index edd2eab0d..4c7492310 100644
--- a/Mailman/database/model/mailinglist.py
+++ b/Mailman/database/model/mailinglist.py
@@ -30,6 +30,7 @@ class MailingList(Entity):
IMailingListAddresses,
IMailingListIdentity,
IMailingListRosters,
+ IMailingListWeb,
)
# List identity
@@ -179,3 +180,14 @@ class MailingList(Entity):
def fqdn_listname(self):
"""See IMailingListIdentity."""
return fqdn_listname(self.list_name, self.host_name)
+
+ @property
+ def web_host(self):
+ """See IMailingListWeb."""
+ return config.domains[self.host_name]
+
+ def script_url(self, target, context=None):
+ """See IMailingListWeb."""
+ # XXX Handle the case for when context is not None; those would be
+ # relative URLs.
+ return self.web_page_url + target + '/' + self.fqdn_listname
diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py
index d9562aede..efb72ee11 100644
--- a/Mailman/database/model/member.py
+++ b/Mailman/database/model/member.py
@@ -19,6 +19,7 @@ from elixir import *
from zope.interface import implements
from Mailman.Utils import split_listname
+from Mailman.constants import SystemDefaultPreferences
from Mailman.database.types import EnumType
from Mailman.interfaces import IMember, IPreferences
@@ -42,3 +43,37 @@ class Member(Entity):
def __repr__(self):
return '<Member: %s on %s as %s>' % (
self.address, self.mailing_list, self.role)
+
+ def _lookup(self, preference):
+ pref = getattr(self.preferences, preference)
+ if pref is not None:
+ return pref
+ pref = getattr(self.address.preferences, preference)
+ if pref is not None:
+ return pref
+ if self.address.user:
+ pref = getattr(self.address.user.preferences, preference)
+ if pref is not None:
+ return pref
+ return getattr(SystemDefaultPreferences, preference)
+
+ @property
+ def delivery_mode(self):
+ return self._lookup('delivery_mode')
+
+ @property
+ def acknowledge_posts(self):
+ return self._lookup('acknowledge_posts')
+
+ @property
+ def preferred_language(self):
+ return self._lookup('preferred_language')
+
+ def unsubscribe(self):
+ self.preferences.delete()
+ self.delete()
+
+ @property
+ def options_url(self):
+ # XXX Um, this is definitely wrong
+ return 'http://example.com/' + self.address.address
diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py
index ee50cddf0..0730d2b4b 100644
--- a/Mailman/database/model/roster.py
+++ b/Mailman/database/model/roster.py
@@ -22,11 +22,12 @@ the ones that fit a particular role. These are used as the member, owner,
moderator, and administrator roster filters.
"""
+from sqlalchemy import *
from zope.interface import implements
from Mailman.constants import DeliveryMode, MemberRole
from Mailman.constants import SystemDefaultPreferences
-from Mailman.database.model import Member
+from Mailman.database.model import Address, Member
from Mailman.interfaces import IRoster
@@ -42,12 +43,16 @@ class AbstractRoster(object):
"""
implements(IRoster)
+ role = None
+
def __init__(self, mlist):
self._mlist = mlist
@property
def members(self):
- raise NotImplementedError
+ for member in Member.select_by(mailing_list=self._mlist.fqdn_listname,
+ role=self.role):
+ yield member
@property
def users(self):
@@ -67,21 +72,27 @@ class AbstractRoster(object):
for member in self.members:
yield member.address
+ def get_member(self, address):
+ results = Member.select(
+ and_(Member.c.mailing_list == self._mlist.fqdn_listname,
+ Member.c.role == self.role,
+ Address.c.address == address,
+ Member.c.address_id == Address.c.id))
+ if len(results) == 0:
+ return None
+ elif len(results) == 1:
+ return results[0]
+ else:
+ assert len(results) <= 1, (
+ 'Too many matching member results: %s' % results)
+
class MemberRoster(AbstractRoster):
"""Return all the members of a list."""
name = 'member'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. XXX we have to use a private
- # data attribute of MailList for now.
- for member in Member.select_by(mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- yield member
+ role = MemberRole.member
@@ -89,15 +100,7 @@ class OwnerRoster(AbstractRoster):
"""Return all the owners of a list."""
name = 'owner'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. XXX we have to use a private
- # data attribute of MailList for now.
- for member in Member.select_by(mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.owner):
- yield member
+ role = MemberRole.owner
@@ -105,15 +108,7 @@ class ModeratorRoster(AbstractRoster):
"""Return all the owners of a list."""
name = 'moderator'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. XXX we have to use a private
- # data attribute of MailList for now.
- for member in Member.select_by(mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.moderator):
- yield member
+ role = MemberRole.moderator
@@ -125,30 +120,31 @@ class AdministratorRoster(AbstractRoster):
@property
def members(self):
# Administrators are defined as the union of the owners and the
- # moderators. Until I figure out a more efficient way of doing this,
- # this will have to do.
- owners = Member.select_by(mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.owner)
- moderators = Member.select_by(mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.moderator)
- members = set(owners)
- members.update(set(moderators))
+ # moderators.
+ members = Member.select(
+ and_(Member.c.mailing_list == self._mlist.fqdn_listname,
+ or_(Member.c.role == MemberRole.owner,
+ Member.c.role == MemberRole.moderator)))
for member in members:
yield member
-
-
-def _delivery_mode(member):
- if member.preferences.delivery_mode is not None:
- return member.preferences.delivery_mode
- if member.address.preferences.delivery_mode is not None:
- return member.address.preferences.delivery_mode
- if (member.address.user and
- member.address.user.preferences.delivery_mode is not None):
- return member.address.user.preferences.delivery_mode
- return SystemDefaultPreferences.delivery_mode
+ def get_member(self, address):
+ results = Member.select(
+ and_(Member.c.mailing_list == self._mlist.fqdn_listname,
+ or_(Member.c.role == MemberRole.moderator,
+ Member.c.role == MemberRole.owner),
+ Address.c.address == address,
+ Member.c.address_id == Address.c.id))
+ if len(results) == 0:
+ return None
+ elif len(results) == 1:
+ return results[0]
+ else:
+ assert len(results) <= 1, (
+ 'Too many matching member results: %s' % results)
+
class RegularMemberRoster(AbstractRoster):
"""Return all the regular delivery members of a list."""
@@ -161,7 +157,7 @@ class RegularMemberRoster(AbstractRoster):
# that have a regular delivery mode.
for member in Member.select_by(mailing_list=self._mlist.fqdn_listname,
role=MemberRole.member):
- if _delivery_mode(member) == DeliveryMode.regular:
+ if member.delivery_mode == DeliveryMode.regular:
yield member
@@ -186,5 +182,5 @@ class DigestMemberRoster(AbstractRoster):
# that have one of the digest delivery modes.
for member in Member.select_by(mailing_list=self._mlist.fqdn_listname,
role=MemberRole.member):
- if _delivery_mode(member) in _digest_modes:
+ if member.delivery_mode in _digest_modes:
yield member
diff --git a/Mailman/docs/acknowledge.txt b/Mailman/docs/acknowledge.txt
new file mode 100644
index 000000000..6c7e3c28f
--- /dev/null
+++ b/Mailman/docs/acknowledge.txt
@@ -0,0 +1,176 @@
+Message acknowledgment
+======================
+
+When a user posts a message to a mailing list, and that user has chosen to
+receive acknowledgments of their postings, Mailman will sent them such an
+acknowledgment.
+
+ >>> from email import message_from_string
+ >>> from Mailman.Message import Message
+ >>> from Mailman.Handlers.Acknowledge import process
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> mlist.real_name = 'XTest'
+ >>> # XXX This will almost certainly change once we've worked out the web
+ >>> # space layout for mailing lists now.
+ >>> mlist._data.web_page_url = 'http://lists.example.com/'
+ >>> flush()
+
+ >>> # Ensure that the virgin queue is empty, since we'll be checking this
+ >>> # for new auto-response messages.
+ >>> from Mailman.Queue.sbcache import get_switchboard
+ >>> virginq = get_switchboard(config.VIRGINQUEUE_DIR)
+ >>> virginq.files()
+ []
+
+Subscribe a user to the mailing list.
+
+ >>> from Mailman.constants import MemberRole
+ >>> user_1 = config.user_manager.create_user('aperson@example.com')
+ >>> address_1 = list(user_1.addresses)[0]
+ >>> address_1.subscribe(mlist, MemberRole.member)
+ <Member: aperson@example.com on _xtest@example.com as MemberRole.member>
+ >>> flush()
+
+
+Non-member posts
+----------------
+
+Non-members can't get acknowledgments of their posts to the mailing list.
+
+ >>> msg = message_from_string("""\
+ ... From: bperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> virginq.files()
+ []
+
+We can also specify the original sender in the message's metadata. If that
+person is also not a member, no acknowledgment will be sent either.
+
+ >>> msg = message_from_string("""\
+ ... From: bperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, dict(original_sender='cperson@example.com'))
+ >>> virginq.files()
+ []
+
+
+No acknowledgment requested
+---------------------------
+
+Unless the user has requested acknowledgments, they will not get one.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> virginq.files()
+ []
+
+Similarly if the original sender is specified in the message metadata, and
+that sender is a member but not one who has requested acknowledgments, none
+will be sent.
+
+ >>> user_2 = config.user_manager.create_user('dperson@example.com')
+ >>> address_2 = list(user_2.addresses)[0]
+ >>> address_2.subscribe(mlist, MemberRole.member)
+ <Member: dperson@example.com on _xtest@example.com as MemberRole.member>
+ >>> flush()
+
+ >>> process(mlist, msg, dict(original_sender='dperson@example.com'))
+ >>> virginq.files()
+ []
+
+
+Requested acknowledgments
+-------------------------
+
+If the member requests acknowledgments, Mailman will send them one when they
+post to the mailing list.
+
+ >>> user_1.preferences.acknowledge_posts = True
+ >>> flush()
+
+The receipt will include the original message's subject in the response body,
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Subject: Something witty and insightful
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+
+ >>> len(virginq.files())
+ 1
+ >>> qmsg, qdata = virginq.dequeue(virginq.files()[0])
+ >>> virginq.files()
+ []
+ >>> # Print only some of the meta data. The rest is uninteresting.
+ >>> qdata['listname']
+ '_xtest@example.com'
+ >>> qdata['recips']
+ ['aperson@example.com']
+ >>> print qmsg.as_string()
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: XTest post acknowledgment
+ From: _xtest-bounces@example.com
+ To: aperson@example.com
+ Message-ID: ...
+ Date: ...
+ Precedence: bulk
+ <BLANKLINE>
+ Your message entitled
+ <BLANKLINE>
+ Something witty and insightful
+ <BLANKLINE>
+ was successfully received by the XTest mailing list.
+ <BLANKLINE>
+ List info page: http://lists.example.com/listinfo/_xtest@example.com
+ Your preferences: http://example.com/aperson@example.com
+ <BLANKLINE>
+
+If there is no subject, then the receipt will use a generic message.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+
+ >>> len(virginq.files())
+ 1
+ >>> qmsg, qdata = virginq.dequeue(virginq.files()[0])
+ >>> virginq.files()
+ []
+ >>> # Print only some of the meta data. The rest is uninteresting.
+ >>> qdata['listname']
+ '_xtest@example.com'
+ >>> qdata['recips']
+ ['aperson@example.com']
+ >>> print qmsg.as_string()
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: XTest post acknowledgment
+ From: _xtest-bounces@example.com
+ To: aperson@example.com
+ Message-ID: ...
+ Date: ...
+ Precedence: bulk
+ <BLANKLINE>
+ Your message entitled
+ <BLANKLINE>
+ (no subject)
+ <BLANKLINE>
+ was successfully received by the XTest mailing list.
+ <BLANKLINE>
+ List info page: http://lists.example.com/listinfo/_xtest@example.com
+ Your preferences: http://example.com/aperson@example.com
+ <BLANKLINE>
diff --git a/Mailman/docs/membership.txt b/Mailman/docs/membership.txt
index 36b1508a8..9b6465d4a 100644
--- a/Mailman/docs/membership.txt
+++ b/Mailman/docs/membership.txt
@@ -168,12 +168,15 @@ Claire will be a regular delivery member but not a digest member.
It's easy to make the list administrators members of the mailing list too.
+ >>> members = []
>>> for address in mlist.administrators.addresses:
- ... address.subscribe(mlist, MemberRole.member)
- <Member: Ben Person <bperson@example.com> on
- _xtest@example.com as MemberRole.member>
- <Member: Anne Person <aperson@example.com> on
- _xtest@example.com as MemberRole.member>
+ ... member = address.subscribe(mlist, MemberRole.member)
+ ... 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>]
>>> flush()
>>> sorted(address.address for address in mlist.members.addresses)
['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
@@ -181,3 +184,57 @@ It's easy to make the list administrators members of the mailing list too.
['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
>>> sorted(address.address for address in mlist.digest_members.addresses)
[]
+
+
+Finding members
+---------------
+
+You can find the IMember object that is a member of a roster for a given text
+email address by using an IRoster's .get_member() method.
+
+ >>> mlist.owners.get_member('aperson@example.com')
+ <Member: Anne Person <aperson@example.com> on
+ _xtest@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>
+ >>> mlist.members.get_member('aperson@example.com')
+ <Member: Anne Person <aperson@example.com> on
+ _xtest@example.com as MemberRole.member>
+
+However, if the address is not subscribed with the appropriate role, then None
+is returned.
+
+ >>> print mlist.administrators.get_member('zperson@example.com')
+ None
+ >>> print mlist.moderators.get_member('aperson@example.com')
+ None
+ >>> print mlist.members.get_member('zperson@example.com')
+ None
+
+
+Clean up
+--------
+
+ >>> for member in mlist.members.members:
+ ... member.unsubscribe()
+ >>> for admin in mlist.administrators.members:
+ ... admin.unsubscribe()
+ >>> flush()
+ >>> list(mlist.members.members)
+ []
+ >>> list(mlist.administrators.members)
+ []
+ >>> for user in config.user_manager.users:
+ ... config.user_manager.delete_user(user)
+ >>> for address in config.user_manager.addresses:
+ ... config.user_manager.delete_address(address)
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
+ >>> flush()
+ >>> list(config.user_manager.users)
+ []
+ >>> list(config.user_manager.addresses)
+ []
+ >>> list(config.list_manager.mailing_lists)
+ []
diff --git a/Mailman/interfaces/member.py b/Mailman/interfaces/member.py
index 5d5e3f717..4bb03f41d 100644
--- a/Mailman/interfaces/member.py
+++ b/Mailman/interfaces/member.py
@@ -32,17 +32,54 @@ class IMember(Interface):
"""The email address that's subscribed to the list.""")
preferences = Attribute(
- """The set of preferences for this subscription.
+ """This member's preferences.""")
- This will return an IPreferences object using the following lookup
- rules:
+ role = Attribute(
+ """The role of this membership.""")
+
+ def unsubscribe():
+ """Unsubscribe (and delete) this member from the mailing list."""
+
+ acknowledge_posts = Attribute(
+ """This is the actual acknowledgment setting for this member.
+
+ Unlike going through the preferences, this attribute return the
+ preference value based on the following lookup order:
- 1. member
- 2. address
- 3. user
- 4. mailing list
- 5. system default
+ 1. The member
+ 2. The address
+ 3. The user
+ 4. System default
""")
- role = Attribute(
- """The role of this membership.""")
+ delivery_mode = Attribute(
+ """This is the actual delivery mode for this member.
+
+ Unlike going through the preferences, this attribute return the
+ preference value based on the following lookup order:
+
+ 1. The member
+ 2. The address
+ 3. The user
+ 4. System default
+ """)
+
+ preferred_language = Attribute(
+ """This is the actual preferred language for this member.
+
+ Unlike going through the preferences, this attribute return the
+ preference value based on the following lookup order:
+
+ 1. The member
+ 2. The address
+ 3. The user
+ 4. System default
+ """)
+
+ options_url = Attribute(
+ """Return the url for the given member's option page.
+
+ XXX This needs a serious re-think in the face of the unified user
+ database, since a member's options aren't tied to any specific mailing
+ list. So in what part of the web-space does the user's options live?
+ """)
diff --git a/Mailman/interfaces/mlistweb.py b/Mailman/interfaces/mlistweb.py
index 16eb94281..728fd1990 100644
--- a/Mailman/interfaces/mlistweb.py
+++ b/Mailman/interfaces/mlistweb.py
@@ -21,19 +21,26 @@ from zope.interface import Interface, Attribute
-class IMailingListURLs(Interface):
+class IMailingListWeb(Interface):
"""The web addresses associated with a mailing list."""
protocol = Attribute(
- """The web protocol to use to contact the server providing the web
- interface for this mailing list, e.g. 'http' or 'https'.""")
+ """The protocol scheme used to contact this list's server.
+
+ The web server on thi protocol provides the web interface for this
+ mailing list. The protocol scheme should be 'http' or 'https'.""")
web_host = Attribute(
- """The read-only domain name of the host to contact for interacting
- with the web interface of the mailing list.""")
+ """This list's web server's domain.
+
+ The read-only domain name of the host to contact for interacting with
+ the web interface of the mailing list.""")
def script_url(target, context=None):
- """Return the url to the given script target. If 'context' is not
- given, or is None, then an absolute url is returned. If context is
- given, it must be an IMailingListRequest object, and the returned url
- will be relative to that object's 'location' attribute."""
+ """Return the url to the given script target.
+
+ If 'context' is not given, or is None, then an absolute url is
+ returned. If context is given, it must be an IMailingListRequest
+ object, and the returned url will be relative to that object's
+ 'location' attribute.
+ """
diff --git a/Mailman/interfaces/roster.py b/Mailman/interfaces/roster.py
index 281e02a05..3b323d0b4 100644
--- a/Mailman/interfaces/roster.py
+++ b/Mailman/interfaces/roster.py
@@ -44,3 +44,10 @@ class IRoster(Interface):
This returns all the addresses for all the users for all the members
managed by this roster.
""")
+
+ def get_member(address):
+ """Return the IMember for the given address.
+
+ 'address' is a text email address. If no matching member is found,
+ None is returned.
+ """
diff --git a/Mailman/interfaces/rosterset.py b/Mailman/interfaces/rosterset.py
deleted file mode 100644
index 12f28bffa..000000000
--- a/Mailman/interfaces/rosterset.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program 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 2
-# of the License, or (at your option) any later version.
-#
-# This program 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 this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Interface for a collection of rosters."""
-
-from zope.interface import Interface, Attribute
-
-
-
-class IRosterSet(Interface):
- """A collection of IRosters."""
-
- serial = Attribute(
- """The unique integer serial number for this roster set.
-
- This is necessary to enforce the separation between the list storage
- and the user/roster storage. You should always reference a roster set
- indirectly through its serial number.""")
-
- rosters = Attribute(
- """An iterator over all the IRosters in this collection.""")
-
- def add(roster):
- """Add the IRoster to this collection.
-
- Does nothing if the roster is already a member of this collection.
- """
-
- def delete(roster):
- """Delete the IRoster from this collection.
-
- Does nothing if the roster is not a member of this collection.
- """
diff --git a/Mailman/database/model/rosterset.py b/Mailman/testing/test_acknowledge.py
index f84b52c15..a40d0c9e4 100644
--- a/Mailman/database/model/rosterset.py
+++ b/Mailman/testing/test_acknowledge.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -15,27 +15,18 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
-from zope.interface import implements
+"""Doctest harness for testing message acknowledgment."""
-from Mailman.interfaces import IRosterSet
+import doctest
+import unittest
-ROSTER_KIND = 'Mailman.database.model.roster.Roster'
+options = (doctest.ELLIPSIS
+ | doctest.NORMALIZE_WHITESPACE
+ | doctest.REPORT_NDIFF)
-
-# Internal implementation of roster sets for use with mailing lists. These
-# are owned by the user storage.
-class RosterSet(Entity):
- implements(IRosterSet)
-
- has_field('name', Unicode)
- has_and_belongs_to_many('rosters', of_kind=ROSTER_KIND)
-
- def add(self, roster):
- if roster not in self.rosters:
- self.rosters.append(roster)
-
- def delete(self, roster):
- if roster in self.rosters:
- self.rosters.remove(roster)
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/acknowledge.txt',
+ optionflags=options))
+ return suite
diff --git a/Mailman/testing/test_handlers.py b/Mailman/testing/test_handlers.py
index f4ad2ba4f..a0a196dae 100644
--- a/Mailman/testing/test_handlers.py
+++ b/Mailman/testing/test_handlers.py
@@ -62,135 +62,6 @@ def password(cleartext):
-class TestAcknowledge(TestBase):
- def setUp(self):
- TestBase.setUp(self)
- # We're going to want to inspect this queue directory
- self._sb = Switchboard(config.VIRGINQUEUE_DIR)
- # Add a member
- self._mlist.addNewMember('aperson@example.org')
- self._mlist.personalize = False
-
- def tearDown(self):
- for f in os.listdir(config.VIRGINQUEUE_DIR):
- os.unlink(os.path.join(config.VIRGINQUEUE_DIR, f))
- TestBase.tearDown(self)
-
- def test_no_ack_msgdata(self):
- eq = self.assertEqual
- # Make sure there are no files in the virgin queue already
- eq(len(self._sb.files()), 0)
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- Acknowledge.process(self._mlist, msg,
- {'original_sender': 'aperson@example.org'})
- eq(len(self._sb.files()), 0)
-
- def test_no_ack_not_a_member(self):
- eq = self.assertEqual
- # Make sure there are no files in the virgin queue already
- eq(len(self._sb.files()), 0)
- msg = email.message_from_string("""\
-From: bperson@example.com
-
-""", Message.Message)
- Acknowledge.process(self._mlist, msg,
- {'original_sender': 'bperson@example.com'})
- eq(len(self._sb.files()), 0)
-
- def test_no_ack_sender(self):
- eq = self.assertEqual
- eq(len(self._sb.files()), 0)
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- Acknowledge.process(self._mlist, msg, {})
- eq(len(self._sb.files()), 0)
-
- def test_ack_no_subject(self):
- eq = self.assertEqual
- self._mlist.setMemberOption(
- 'aperson@example.org', config.AcknowledgePosts, 1)
- eq(len(self._sb.files()), 0)
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- Acknowledge.process(self._mlist, msg, {})
- files = self._sb.files()
- eq(len(files), 1)
- qmsg, qdata = self._sb.dequeue(files[0])
- # Check the .db file
- eq(qdata.get('listname'), '_xtest@example.com')
- eq(qdata.get('recips'), ['aperson@example.org'])
- eq(qdata.get('version'), 3)
- # Check the .pck
- eq(str(qmsg['subject']), '_xtest post acknowledgement')
- eq(qmsg['to'], 'aperson@example.org')
- eq(qmsg['from'], '_xtest-bounces@example.com')
- eq(qmsg.get_content_type(), 'text/plain')
- eq(qmsg.get_param('charset'), 'us-ascii')
- msgid = qmsg['message-id']
- self.failUnless(msgid.startswith('<mailman.'))
- self.failUnless(msgid.endswith('._xtest@example.com>'))
- eq(qmsg.get_payload(), """\
-Your message entitled
-
- (no subject)
-
-was successfully received by the _xtest mailing list.
-
-List info page: http://www.example.com/mailman/listinfo/_xtest@example.com
-Your preferences: http://www.example.com/mailman/options/_xtest@example.com/aperson%40example.org
-""")
- # Make sure we dequeued the only message
- eq(len(self._sb.files()), 0)
-
- def test_ack_with_subject(self):
- eq = self.assertEqual
- self._mlist.setMemberOption(
- 'aperson@example.org', config.AcknowledgePosts, 1)
- eq(len(self._sb.files()), 0)
- msg = email.message_from_string("""\
-From: aperson@example.org
-Subject: Wish you were here
-
-""", Message.Message)
- Acknowledge.process(self._mlist, msg, {})
- files = self._sb.files()
- eq(len(files), 1)
- qmsg, qdata = self._sb.dequeue(files[0])
- # Check the .db file
- eq(qdata.get('listname'), '_xtest@example.com')
- eq(qdata.get('recips'), ['aperson@example.org'])
- eq(qdata.get('version'), 3)
- # Check the .pck
- eq(str(qmsg['subject']), '_xtest post acknowledgement')
- eq(qmsg['to'], 'aperson@example.org')
- eq(qmsg['from'], '_xtest-bounces@example.com')
- eq(qmsg.get_content_type(), 'text/plain')
- eq(qmsg.get_param('charset'), 'us-ascii')
- msgid = qmsg['message-id']
- self.failUnless(msgid.startswith('<mailman.'))
- self.failUnless(msgid.endswith('._xtest@example.com>'))
- eq(qmsg.get_payload(), """\
-Your message entitled
-
- Wish you were here
-
-was successfully received by the _xtest mailing list.
-
-List info page: http://www.example.com/mailman/listinfo/_xtest@example.com
-Your preferences: http://www.example.com/mailman/options/_xtest@example.com/aperson%40example.org
-""")
- # Make sure we dequeued the only message
- eq(len(self._sb.files()), 0)
-
-
-
class TestAfterDelivery(TestBase):
# Both msg and msgdata are ignored
def test_process(self):