summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/database/address.py8
-rw-r--r--src/mailman/database/digests.py53
-rw-r--r--src/mailman/database/mailinglist.py21
-rw-r--r--src/mailman/database/mailman.sql12
-rw-r--r--src/mailman/database/roster.py59
-rw-r--r--src/mailman/docs/addresses.txt3
-rw-r--r--src/mailman/docs/membership.txt2
-rw-r--r--src/mailman/interfaces/digests.py42
-rw-r--r--src/mailman/interfaces/mailinglist.py31
-rw-r--r--src/mailman/queue/digest.py18
-rw-r--r--src/mailman/queue/docs/digester.txt195
-rw-r--r--src/mailman/styles/default.py1
12 files changed, 349 insertions, 96 deletions
diff --git a/src/mailman/database/address.py b/src/mailman/database/address.py
index 528d3af51..f63f03e14 100644
--- a/src/mailman/database/address.py
+++ b/src/mailman/database/address.py
@@ -26,10 +26,9 @@ __all__ = [
from email.utils import formataddr
-from storm.locals import *
+from storm.locals import DateTime, Int, Reference, Store, Unicode
from zope.interface import implements
-from mailman.config import config
from mailman.database.member import Member
from mailman.database.model import Model
from mailman.database.preferences import Preferences
@@ -76,7 +75,8 @@ class Address(Model):
def subscribe(self, mailing_list, role):
# This member has no preferences by default.
- member = config.db.store.find(
+ store = Store.of(self)
+ member = store.find(
Member,
Member.role == role,
Member.mailing_list == mailing_list.fqdn_listname,
@@ -88,7 +88,7 @@ class Address(Model):
mailing_list=mailing_list.fqdn_listname,
address=self)
member.preferences = Preferences()
- config.db.store.add(member)
+ store.add(member)
return member
@property
diff --git a/src/mailman/database/digests.py b/src/mailman/database/digests.py
new file mode 100644
index 000000000..291dafa28
--- /dev/null
+++ b/src/mailman/database/digests.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2009 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/>.
+
+"""One last digest."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'OneLastDigest',
+ ]
+
+
+from storm.locals import Int, Reference
+from zope.interface import implements
+
+from mailman.database.model import Model
+from mailman.database.types import Enum
+from mailman.interfaces.digests import IOneLastDigest
+
+
+
+class OneLastDigest(Model):
+ implements(IOneLastDigest)
+
+ id = Int(primary=True)
+
+ mailing_list_id = Int()
+ mailing_list = Reference(mailing_list_id, 'MailingList.id')
+
+ address_id = Int()
+ address = Reference(address_id, 'Address.id')
+
+ delivery_mode = Enum()
+
+ def __init__(self, mailing_list, address, delivery_mode):
+ self.mailing_list = mailing_list
+ self.address = address
+ self.delivery_mode = delivery_mode
diff --git a/src/mailman/database/mailinglist.py b/src/mailman/database/mailinglist.py
index 9141ad64c..2956cac57 100644
--- a/src/mailman/database/mailinglist.py
+++ b/src/mailman/database/mailinglist.py
@@ -28,12 +28,14 @@ __all__ = [
import os
import string
-from storm.locals import *
+from storm.locals import (
+ Bool, DateTime, Float, Int, Pickle, Store, TimeDelta, Unicode)
from urlparse import urljoin
from zope.interface import implements
from mailman.config import config
from mailman.database import roster
+from mailman.database.digests import OneLastDigest
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.mailinglist import IMailingList, Personalization
@@ -63,7 +65,6 @@ class MailingList(Model):
next_request_id = Int()
next_digest_number = Int()
digest_last_sent_at = DateTime()
- one_last_digest = Pickle()
volume = Int()
last_post_time = DateTime()
# Attributes which are directly modifiable via the web u/i. The more
@@ -276,3 +277,19 @@ class MailingList(Model):
self._preferred_language = language.code
except AttributeError:
self._preferred_language = language
+
+ def send_one_last_digest_to(self, address, delivery_mode):
+ """See `IMailingList`."""
+ digest = OneLastDigest(self, address, delivery_mode)
+ Store.of(self).add(digest)
+
+ @property
+ def last_digest_recipients(self):
+ """See `IMailingList`."""
+ results = Store.of(self).find(
+ OneLastDigest,
+ OneLastDigest.mailing_list == self)
+ recipients = [(digest.address, digest.delivery_mode)
+ for digest in results]
+ results.remove()
+ return recipients
diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql
index c46fb193e..79a28574e 100644
--- a/src/mailman/database/mailman.sql
+++ b/src/mailman/database/mailman.sql
@@ -54,7 +54,6 @@ CREATE TABLE mailinglist (
next_request_id INTEGER,
next_digest_number INTEGER,
digest_last_sent_at TIMESTAMP,
- one_last_digest BLOB,
volume INTEGER,
last_post_time TIMESTAMP,
accept_these_nonmembers BLOB,
@@ -178,6 +177,17 @@ CREATE TABLE message (
message_id TEXT,
PRIMARY KEY (id)
);
+CREATE TABLE onelastdigest (
+ id INTEGER NOT NULL,
+ mailing_list_id INTEGER,
+ address_id INTEGER,
+ delivery_mode TEXT,
+ PRIMARY KEY (id),
+ CONSTRAINT onelastdigest_mailing_list_id_fk
+ FOREIGN KEY(mailing_list_id) REFERENCES mailinglist(id),
+ CONSTRAINT onelastdigest_address_id_fk
+ FOREIGN KEY(address_id) REFERENCES address(id)
+ );
CREATE TABLE pended (
id INTEGER NOT NULL,
token TEXT,
diff --git a/src/mailman/database/roster.py b/src/mailman/database/roster.py
index fc0a24c7d..3b8ba4c4c 100644
--- a/src/mailman/database/roster.py
+++ b/src/mailman/database/roster.py
@@ -37,12 +37,13 @@ __all__ = [
]
-from storm.locals import *
+from storm.expr import And, LeftJoin, Or
from zope.interface import implements
from mailman.config import config
from mailman.database.address import Address
from mailman.database.member import Member
+from mailman.database.preferences import Preferences
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.roster import IRoster
@@ -167,49 +168,49 @@ class AdministratorRoster(AbstractRoster):
-class RegularMemberRoster(AbstractRoster):
+class DeliveryMemberRoster(AbstractRoster):
+ """Return all the members having a particular kind of delivery."""
+
+ def _get_members(self, *delivery_modes):
+ """The set of members for a mailing list, filter by delivery mode.
+
+ :param delivery_modes: The modes to filter on.
+ :type delivery_modes: sequence of `DeliveryMode`.
+ :return: A generator of members.
+ :rtype: generator
+ """
+ results = config.db.store.find(
+ Member,
+ And(Member.mailing_list == self._mlist.fqdn_listname,
+ Member.role == MemberRole.member))
+ for member in results:
+ if member.delivery_mode in delivery_modes:
+ yield member
+
+
+class RegularMemberRoster(DeliveryMemberRoster):
"""Return all the regular delivery members of a list."""
name = 'regular_members'
@property
def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. Then return only those members
- # that have a regular delivery mode.
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- if member.delivery_mode == DeliveryMode.regular:
- yield member
+ for member in self._get_members(DeliveryMode.regular):
+ yield member
-_digest_modes = (
- DeliveryMode.mime_digests,
- DeliveryMode.plaintext_digests,
- DeliveryMode.summary_digests,
- )
-
-
-
-class DigestMemberRoster(AbstractRoster):
+class DigestMemberRoster(DeliveryMemberRoster):
"""Return all the regular delivery members of a list."""
name = 'digest_members'
@property
def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. Then return only those members
- # that have one of the digest delivery modes.
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- if member.delivery_mode in _digest_modes:
- yield member
+ for member in self._get_members(DeliveryMode.plaintext_digests,
+ DeliveryMode.mime_digests,
+ DeliveryMode.summary_digests):
+ yield member
diff --git a/src/mailman/docs/addresses.txt b/src/mailman/docs/addresses.txt
index 9eccb2673..a8ae24840 100644
--- a/src/mailman/docs/addresses.txt
+++ b/src/mailman/docs/addresses.txt
@@ -148,7 +148,8 @@ subscribed, a role is specified.
>>> address_5 = usermgr.create_address(
... u'eperson@example.com', u'Elly Person')
- >>> mlist = config.db.list_manager.create(u'_xtext@example.com')
+ >>> mlist = create_list(u'_xtext@example.com')
+
>>> from mailman.interfaces.member import MemberRole
>>> address_5.subscribe(mlist, MemberRole.owner)
<Member: Elly Person <eperson@example.com> on
diff --git a/src/mailman/docs/membership.txt b/src/mailman/docs/membership.txt
index 7f9f16738..65f5b37ed 100644
--- a/src/mailman/docs/membership.txt
+++ b/src/mailman/docs/membership.txt
@@ -2,7 +2,7 @@ List memberships
================
Users represent people in Mailman. Users control email addresses, and rosters
-are collectons of members. A member gives an email address a role, such as
+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.
diff --git a/src/mailman/interfaces/digests.py b/src/mailman/interfaces/digests.py
new file mode 100644
index 000000000..009be8e70
--- /dev/null
+++ b/src/mailman/interfaces/digests.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2009 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/>.
+
+"""One last digest."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IOneLastDigest'
+ ]
+
+
+from zope.interface import Interface, Attribute
+
+
+
+class IOneLastDigest(Interface):
+ """Users who should receive one last digest."""
+
+ mailing_list = Attribute(
+ """The mailing list for the one last digest.""")
+
+ address = Attribute(
+ """The address to receive the one last digest.""")
+
+ delivery_mode = Attribute(
+ """The digest delivery mode to send.""")
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 2885f60ab..4e04fa39b 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -216,25 +216,30 @@ class IMailingList(Interface):
the digest volume number is bumped, the digest number is reset to
1.""")
- message_count = Attribute(
- """The number of messages in the digest currently being collected.""")
-
digest_size_threshold = Attribute(
"""The maximum (approximate) size in kilobytes of the digest currently
being collected.""")
- messages = Attribute(
- """An iterator over all the messages in the digest currently being
- created. Returns individual IPostedMessage objects.
- """)
+ def send_one_last_digest_to(address, delivery_mode):
+ """Make sure to send one last digest to an address.
+
+ This is used when a person transitions from digest delivery to regular
+ delivery and wants to make sure they don't miss anything. By
+ indicating that they'd like to receive one last digest, they will
+ ensure continuity in receiving mailing lists posts.
- limits = Attribute(
- """An iterator over the IDigestLimiters associated with this digest.
- Each limiter can make a determination of whether the digest has
- reached the threshold for being automatically sent.""")
+ :param address: The address of the person receiving one last digest.
+ :type address: `IAddress`
+ :param delivery_mode: The type of digest to receive.
+ :type delivery_mode: `DeliveryMode`
+ """
+
+ last_digest_recipients = Attribute(
+ """An iterator over the addresses that should receive one last digest.
- def send():
- """Send this digest now."""
+ Items are 2-tuples of (`IAddress`, `DeliveryMode`). The one last
+ digest recipients are cleared.
+ """)
decorators = Attribute(
"""An iterator over all the IDecorators associated with this digest.
diff --git a/src/mailman/queue/digest.py b/src/mailman/queue/digest.py
index a590a9997..30705b8be 100644
--- a/src/mailman/queue/digest.py
+++ b/src/mailman/queue/digest.py
@@ -43,6 +43,7 @@ from mailman.Utils import maketext, oneline, wrap
from mailman.config import config
from mailman.core.errors import DiscardMessage
from mailman.i18n import _
+from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.pipeline.decorate import decorate
from mailman.pipeline.scrubber import process as scrubber
from mailman.queue import Runner
@@ -332,10 +333,6 @@ class DigestRunner(Runner):
# digest to ensure that there will be no gaps in the messages they
# receive.
digest_members = set(mlist.digest_members.members)
- for address in mlist.one_last_digest:
- member = mlist.digest_members.get_member(address)
- if member:
- digest_members.add(member)
for member in digest_members:
if member.delivery_status <> DeliveryStatus.enabled:
continue
@@ -350,6 +347,16 @@ class DigestRunner(Runner):
raise AssertionError(
'Digest member "{0}" unexpected delivery mode: {1}'.format(
email_address, member.delivery_mode))
+ # Add also the folks who are receiving one last digest.
+ for address, delivery_mode in mlist.last_digest_recipients:
+ if delivery_mode == DeliveryMode.plaintext_digests:
+ rfc1153_recipients.add(address.original_address)
+ elif delivery_mode == DeliveryMode.mime_digests:
+ mime_recipients.add(address.original_address)
+ else:
+ raise AssertionError(
+ 'OLD recipient "{0}" unexpected delivery mode: {1}'.format(
+ address, delivery_mode))
# Send the digests to the virgin queue for final delivery.
queue = config.switchboards['virgin']
queue.enqueue(mime,
@@ -360,6 +367,3 @@ class DigestRunner(Runner):
recips=rfc1153_recipients,
listname=mlist.fqdn_listname,
isdigest=True)
- # Now that we've delivered the last digest to folks who were waiting
- # for it, clear that recipient set.
- mlist.one_last_digest.clear()
diff --git a/src/mailman/queue/docs/digester.txt b/src/mailman/queue/docs/digester.txt
index 57e80317c..2c9c813b2 100644
--- a/src/mailman/queue/docs/digester.txt
+++ b/src/mailman/queue/docs/digester.txt
@@ -8,23 +8,27 @@ This starts by a number of messages being posted to the mailing list.
>>> mlist.digest_size_threshold = 0.5
>>> mlist.volume = 1
>>> mlist.next_digest_number = 1
- >>> size = 0
>>> from string import Template
>>> process = config.handlers['to-digest'].process
- >>> for i in range(1, 5):
- ... text = Template("""\
+
+ >>> def fill_digest():
+ ... size = 0
+ ... for i in range(1, 5):
+ ... text = Template("""\
... From: aperson@example.com
... To: xtest@example.com
... Subject: Test message $i
...
... Here is message $i
... """).substitute(i=i)
- ... msg = message_from_string(text)
- ... process(mlist, msg, {})
- ... size += len(text)
- ... if size >= mlist.digest_size_threshold * 1024:
- ... break
+ ... msg = message_from_string(text)
+ ... process(mlist, msg, {})
+ ... size += len(text)
+ ... if size >= mlist.digest_size_threshold * 1024:
+ ... break
+
+ >>> fill_digest()
The queue runner gets kicked off when a marker message gets dropped into the
digest queue. The message metadata points to the mailbox file containing the
@@ -77,16 +81,16 @@ delivery.
The MIME digest is a multipart, and the RFC 1153 digest is the other one.
- >>> if messages[0].msg.is_multipart():
- ... mime = messages[0].msg
- ... rfc1153 = messages[1].msg
- ... else:
- ... mime = messages[1].msg
- ... rfc1153 = messages[0].msg
+ >>> def mime_rfc1153(messages):
+ ... if messages[0].msg.is_multipart():
+ ... return messages[0], messages[1]
+ ... return messages[1], messages[0]
+
+ >>> mime, rfc1153 = mime_rfc1153(messages)
The MIME digest has lots of good stuff, all contained in the multipart.
- >>> print mime.as_string()
+ >>> print mime.msg.as_string()
Content-Type: multipart/mixed; boundary="===============...=="
MIME-Version: 1.0
From: test-request@example.com
@@ -103,15 +107,15 @@ The MIME digest has lots of good stuff, all contained in the multipart.
Content-Description: Test Digest, Vol 1, Issue 1
<BLANKLINE>
Send Test mailing list submissions to
- test@example.com
+ test@example.com
<BLANKLINE>
To subscribe or unsubscribe via the World Wide Web, visit
- http://lists.example.com/listinfo/test@example.com
+ http://lists.example.com/listinfo/test@example.com
or, via email, send a message with subject or body 'help' to
- test-request@example.com
+ test-request@example.com
<BLANKLINE>
You can reach the person managing the list at
- test-owner@example.com
+ test-owner@example.com
<BLANKLINE>
When replying, please edit your Subject line so it is more specific
than "Re: Contents of Test digest..."
@@ -184,7 +188,7 @@ The MIME digest has lots of good stuff, all contained in the multipart.
The RFC 1153 contains the digest in a single plain text message.
- >>> print rfc1153.as_string()
+ >>> print rfc1153.msg.as_string()
From: test-request@example.com
Subject: Test Digest, Vol 1, Issue 1
To: test@example.com
@@ -196,15 +200,15 @@ The RFC 1153 contains the digest in a single plain text message.
Content-Transfer-Encoding: 7bit
<BLANKLINE>
Send Test mailing list submissions to
- test@example.com
+ test@example.com
<BLANKLINE>
To subscribe or unsubscribe via the World Wide Web, visit
- http://lists.example.com/listinfo/test@example.com
+ http://lists.example.com/listinfo/test@example.com
or, via email, send a message with subject or body 'help' to
- test-request@example.com
+ test-request@example.com
<BLANKLINE>
You can reach the person managing the list at
- test-owner@example.com
+ test-owner@example.com
<BLANKLINE>
When replying, please edit your Subject line so it is more specific
than "Re: Contents of Test digest..."
@@ -324,16 +328,11 @@ queue.
One of which is the MIME digest and the other of which is the RFC 1153 digest.
- >>> if messages[0].msg.is_multipart():
- ... mime = messages[0].msg
- ... rfc1153 = messages[1].msg
- ... else:
- ... mime = messages[1].msg
- ... rfc1153 = messages[0].msg
+ >>> mime, rfc1153 = mime_rfc1153(messages)
You can see that the digests contain a mix of French and Japanese.
- >>> print mime.as_string()
+ >>> print mime.msg.as_string()
Content-Type: multipart/mixed; boundary="===============...=="
MIME-Version: 1.0
From: test-request@example.com
@@ -350,17 +349,17 @@ You can see that the digests contain a mix of French and Japanese.
Content-Description: Groupe Test, Vol. 1, Parution 2
<BLANKLINE>
Envoyez vos messages pour la liste Test =E0
- test@example.com
+ test@example.com
<BLANKLINE>
Pour vous (d=E9s)abonner par le web, consultez
- http://lists.example.com/listinfo/test@example.com
+ http://lists.example.com/listinfo/test@example.com
<BLANKLINE>
ou, par courriel, envoyez un message avec =AB=A0help=A0=BB dans le corps ou
dans le sujet =E0
- test-request@example.com
+ test-request@example.com
<BLANKLINE>
Vous pouvez contacter l'administrateur de la liste =E0 l'adresse
- test-owner@example.com
+ test-owner@example.com
<BLANKLINE>
Si vous r=E9pondez, n'oubliez pas de changer l'objet du message afin
qu'il soit plus sp=E9cifique que =AB=A0Re: Contenu du groupe de Test...=A0=
@@ -402,7 +401,7 @@ You can see that the digests contain a mix of French and Japanese.
The RFC 1153 digest will be encoded in UTF-8 since it contains a mixture of
French and Japanese characters.
- >>> print rfc1153.as_string()
+ >>> print rfc1153.msg.as_string()
From: test-request@example.com
Subject: Groupe Test, Vol. 1, Parution 2
To: test@example.com
@@ -420,7 +419,8 @@ The content can be decoded to see the actual digest text.
# We must display the repr of the decoded value because doctests cannot
# handle the non-ascii characters.
- >>> [repr(line) for line in rfc1153.get_payload(decode=True).splitlines()]
+ >>> [repr(line)
+ ... for line in rfc1153.msg.get_payload(decode=True).splitlines()]
["'Envoyez vos messages pour la liste Test \\xc3\\xa0'",
"'\\ttest@example.com'",
"''",
@@ -462,3 +462,124 @@ The content can be decoded to see the actual digest text.
"''",
"'Fin de Groupe Test, Vol. 1, Parution 2'",
"'**************************************'"]
+
+ >>> config.pop('french')
+
+
+Digest delivery
+---------------
+
+A mailing list's members can choose to receive normal delivery, plain text
+digests, or MIME digests.
+
+ >>> len(get_queue_messages('virgin'))
+ 0
+
+ >>> from mailman.interfaces.member import DeliveryMode, MemberRole
+ >>> def subscribe(email, mode):
+ ... address = config.db.user_manager.create_address(email)
+ ... member = address.subscribe(mlist, MemberRole.member)
+ ... member.preferences.delivery_mode = mode
+ ... return member
+
+Two regular delivery members subscribe to the mailing list.
+
+ >>> member_1 = subscribe(u'uperson@example.com', DeliveryMode.regular)
+ >>> member_2 = subscribe(u'vperson@example.com', DeliveryMode.regular)
+
+Two MIME digest members subscribe to the mailing list.
+
+ >>> member_3 = subscribe(u'wperson@example.com', DeliveryMode.mime_digests)
+ >>> member_4 = subscribe(u'xperson@example.com', DeliveryMode.mime_digests)
+
+One RFC 1153 digest member subscribes to the mailing list.
+
+ >>> member_5 = subscribe(
+ ... u'yperson@example.com', DeliveryMode.plaintext_digests)
+ >>> member_6 = subscribe(
+ ... u'zperson@example.com', DeliveryMode.plaintext_digests)
+
+When a digest gets sent, the appropriate recipient list is chosen.
+
+ >>> mlist.preferred_language = u'en'
+ >>> mlist.digest_size_threshold = 0.5
+ >>> fill_digest()
+ >>> runner.run()
+
+The digests are sitting in the virgin queue. One of them is the MIME digest
+and the other is the RFC 1153 digest.
+
+ >>> messages = get_queue_messages('virgin')
+ >>> len(messages)
+ 2
+
+ >>> mime, rfc1153 = mime_rfc1153(messages)
+
+Only wperson and xperson get the MIME digests.
+
+ >>> sorted(mime.msgdata['recips'])
+ [u'wperson@example.com', u'xperson@example.com']
+
+Only yperson and zperson get the RFC 1153 digests.
+
+ >>> sorted(rfc1153.msgdata['recips'])
+ [u'yperson@example.com', u'zperson@example.com']
+
+Now uperson decides that they would like to start receiving digests too.
+
+ >>> member_1.preferences.delivery_mode = DeliveryMode.mime_digests
+ >>> fill_digest()
+ >>> runner.run()
+
+ >>> messages = get_queue_messages('virgin')
+ >>> len(messages)
+ 2
+
+ >>> mime, rfc1153 = mime_rfc1153(messages)
+ >>> sorted(mime.msgdata['recips'])
+ [u'uperson@example.com', u'wperson@example.com', u'xperson@example.com']
+
+ >>> sorted(rfc1153.msgdata['recips'])
+ [u'yperson@example.com', u'zperson@example.com']
+
+At this point, both uperson and wperson decide that they'd rather receive
+regular deliveries instead of digests. uperson would like to get any last
+digest that may be sent so that she doesn't miss anything. wperson does care
+as much and does not want to receive one last digest.
+
+ >>> mlist.send_one_last_digest_to(
+ ... member_1.address, member_1.preferences.delivery_mode)
+
+ >>> member_1.preferences.delivery_mode = DeliveryMode.regular
+ >>> member_3.preferences.delivery_mode = DeliveryMode.regular
+
+ >>> fill_digest()
+ >>> runner.run()
+
+ >>> messages = get_queue_messages('virgin')
+ >>> len(messages)
+ 2
+
+ >>> mime, rfc1153 = mime_rfc1153(messages)
+ >>> sorted(mime.msgdata['recips'])
+ [u'uperson@example.com', u'xperson@example.com']
+
+ >>> sorted(rfc1153.msgdata['recips'])
+ [u'yperson@example.com', u'zperson@example.com']
+
+Since uperson has received their last digest, they will not get any more of
+them.
+
+ >>> fill_digest()
+ >>> runner.run()
+
+ >>> messages = get_queue_messages('virgin')
+ >>> len(messages)
+ 2
+
+ >>> mime, rfc1153 = mime_rfc1153(messages)
+ >>> sorted(mime.msgdata['recips'])
+ [u'xperson@example.com']
+
+ >>> sorted(rfc1153.msgdata['recips'])
+ [u'yperson@example.com', u'zperson@example.com']
diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py
index 842517c9f..adea20582 100644
--- a/src/mailman/styles/default.py
+++ b/src/mailman/styles/default.py
@@ -123,7 +123,6 @@ $fqdn_listname
${listinfo_page}
"""
mlist.digest_volume_frequency = DigestFrequency.monthly
- mlist.one_last_digest = {}
mlist.next_digest_number = 1
mlist.nondigestable = True
mlist.personalize = Personalization.none