Blob Blame History Raw
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py
index e3d923946..2ebb6dd4b 100644
--- a/src/mailman/app/bounces.py
+++ b/src/mailman/app/bounces.py
@@ -77,7 +77,7 @@ def bounce_message(mlist, msg, error=None):
               else str(error))
     # Currently we always craft bounces as MIME messages.
     bmsg = UserNotification(msg.sender, mlist.owner_address, subject,
-                            lang=mlist.preferred_language)
+                            lang=mlist.preferred_language, role='poster')
     # BAW: Be sure you set the type before trying to attach, or you'll get
     # a MultipartConversionError.
     bmsg.set_type('multipart/mixed')
@@ -222,8 +222,9 @@ def send_probe(member, msg):
     # Craft the probe message.  This will be a multipart where the first part
     # is the probe text and the second part is the message that caused this
     # probe to be sent.
-    probe = UserNotification(member.address.email, probe_sender,
-                             subject, lang=member.preferred_language)
+    probe = UserNotification(
+        member.address.email, probe_sender, subject,
+        lang=member.preferred_language, role='subscriber')
     probe.set_type('multipart/mixed')
     notice = MIMEText(text, _charset=mlist.preferred_language.charset)
     probe.attach(notice)
diff --git a/src/mailman/app/docs/bounces.rst b/src/mailman/app/docs/bounces.rst
index b25381031..fadee9e92 100644
--- a/src/mailman/app/docs/bounces.rst
+++ b/src/mailman/app/docs/bounces.rst
@@ -38,10 +38,12 @@ to the original message author.
     Subject: Something important
     From: ant-owner@example.com
     To: aperson@example.com
+    List-Administrivia: poster
     MIME-Version: 1.0
     Content-Type: multipart/mixed; boundary="..."
     Message-ID: ...
     Date: ...
+    List-Id: <ant.example.com>
     Precedence: bulk
     <BLANKLINE>
     --...
@@ -74,10 +76,12 @@ passed in as an instance of a ``RejectMessage`` exception.
     Subject: Something important
     From: ant-owner@example.com
     To: aperson@example.com
+    List-Administrivia: poster
     MIME-Version: 1.0
     Content-Type: multipart/mixed; boundary="..."
     Message-ID: ...
     Date: ...
+    List-Id: <ant.example.com>
     Precedence: bulk
     <BLANKLINE>
     --...
@@ -114,10 +118,12 @@ be interpolated into the message using the ``{reasons}`` placeholder.
     Subject: Something important
     From: ant-owner@example.com
     To: aperson@example.com
+    List-Administrivia: poster
     MIME-Version: 1.0
     Content-Type: multipart/mixed; boundary="..."
     Message-ID: ...
     Date: ...
+    List-Id: <ant.example.com>
     Precedence: bulk
     <BLANKLINE>
     --...
diff --git a/src/mailman/app/docs/message.rst b/src/mailman/app/docs/message.rst
index 658bf4e30..e87facb79 100644
--- a/src/mailman/app/docs/message.rst
+++ b/src/mailman/app/docs/message.rst
@@ -39,8 +39,10 @@ The message will end up in the `virgin` queue.
     Subject: Something you need to know
     From: test@example.com
     To: aperson@example.com
+    List-Administrivia: subscriber
     Message-ID: ...
     Date: ...
+    List-Id: <test.example.com>
     Precedence: bulk
     <BLANKLINE>
     I needed to tell you this.
diff --git a/src/mailman/app/docs/moderator.rst b/src/mailman/app/docs/moderator.rst
index ce25a4711..a1f4171cb 100644
--- a/src/mailman/app/docs/moderator.rst
+++ b/src/mailman/app/docs/moderator.rst
@@ -329,7 +329,8 @@ There's now a message in the virgin queue, destined for the list owner.
     MIME-Version: 1.0
     ...
     Subject: New subscription request to A Test List from gwen@example.com
-    From: ant-owner@example.com
+    From: noreply@example.com
+    List-Administrivia: list-owner
     To: ant-owner@example.com
     ...
     Your authorization is required for a mailing list subscription request
@@ -349,7 +350,8 @@ Jeff is a member of the mailing list, and chooses to unsubscribe.
     MIME-Version: 1.0
     ...
     Subject: New unsubscription request from A Test List by jeff@example.org
-    From: ant-owner@example.com
+    From: noreply@example.com
+    List-Administrivia: list-owner
     To: ant-owner@example.com
     ...
     Your authorization is required for a mailing list unsubscription
@@ -380,6 +382,7 @@ receive a membership change notice.
     ...
     Subject: A Test List subscription notification
     From: noreply@example.com
+    List-Administrivia: list-owner
     To: ant-owner@example.com
     ...
     Gwen Person <gwen@example.com> has been successfully subscribed to A
@@ -398,6 +401,7 @@ get a notification.
     ...
     Subject: A Test List unsubscription notification
     From: noreply@example.com
+    List-Administrivia: list-owner
     To: ant-owner@example.com
     ...
     Gwen Person <gwen@example.com> has been removed from A Test List.
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index 43763e248..9a809d620 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -24,7 +24,7 @@ from email.utils import formatdate, getaddresses, make_msgid
 from mailman.app.membership import delete_member
 from mailman.config import config
 from mailman.core.i18n import _
-from mailman.email.message import UserNotification
+from mailman.email.message import OwnerNotification, UserNotification
 from mailman.interfaces.action import Action
 from mailman.interfaces.listmanager import ListDeletingEvent
 from mailman.interfaces.member import NotAMemberError
@@ -163,7 +163,7 @@ def handle_message(mlist, id, action, comment=None, forward=None):
             fmsg = UserNotification(
                 addresses, mlist.bounces_address,
                 _('Forward of moderated message'),
-                lang=language)
+                lang=language, role='moderator')
         fmsg.set_type('message/rfc822')
         fmsg.attach(msg)
         fmsg.send(mlist)
@@ -202,9 +202,7 @@ def hold_unsubscription(mlist, email):
             )))
         # This message should appear to come from the <list>-owner so as
         # to avoid any useless bounce processing.
-        msg = UserNotification(
-            mlist.owner_address, mlist.owner_address,
-            subject, text, mlist.preferred_language)
+        msg = OwnerNotification(mlist, subject, text, mlist.owners)
         msg.send(mlist)
     return request_id
 
@@ -268,7 +266,8 @@ def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None):
                  str(origmsg)
                  ])
         subject = _('Request to mailing list "$display_name" rejected')
-    msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
+    msg = UserNotification(
+        recip, mlist.bounces_address, subject, text, lang, 'subscriber')
     msg.send(mlist)
 
 
diff --git a/src/mailman/app/notifications.py b/src/mailman/app/notifications.py
index 4a7bd16fb..1a3b82b46 100644
--- a/src/mailman/app/notifications.py
+++ b/src/mailman/app/notifications.py
@@ -68,7 +68,7 @@ def send_welcome_message(mlist, member, language, text=''):
         formataddr((display_name, member.address.email)),
         mlist.request_address,
         _('Welcome to the "$mlist.display_name" mailing list${digmode}'),
-        text, language)
+        text, language, 'subscriber')
     msg['X-No-Archive'] = 'yes'
     msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
 
@@ -93,7 +93,7 @@ def send_goodbye_message(mlist, address, language):
         address, mlist.bounces_address,
         _('You have been unsubscribed from the $mlist.display_name '
           'mailing list'),
-        goodbye_message, language)
+        goodbye_message, language, 'subscriber')
     msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
 
 
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index 6353eee70..e192485ad 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -27,7 +27,7 @@ from mailman.app.membership import delete_member
 from mailman.app.workflow import Workflow
 from mailman.core.i18n import _
 from mailman.database.transaction import flush
-from mailman.email.message import UserNotification
+from mailman.email.message import OwnerNotification, UserNotification
 from mailman.interfaces.address import IAddress
 from mailman.interfaces.bans import IBanManager
 from mailman.interfaces.listmanager import ListDeletingEvent
@@ -301,9 +301,8 @@ class SubscriptionWorkflow(_SubscriptionWorkflowCommon):
                 )))
             # This message should appear to come from the <list>-owner so as
             # to avoid any useless bounce processing.
-            msg = UserNotification(
-                self.mlist.owner_address, self.mlist.owner_address,
-                subject, text, self.mlist.preferred_language)
+            msg = OwnerNotification(
+                self.mlist, subject, text, self.mlist.owners)
             msg.send(self.mlist)
         # The workflow must stop running here.
         raise StopIteration
@@ -449,9 +448,8 @@ class UnSubscriptionWorkflow(_SubscriptionWorkflowCommon):
                 )))
             # This message should appear to come from the <list>-owner so as
             # to avoid any useless bounce processing.
-            msg = UserNotification(
-                self.mlist.owner_address, self.mlist.owner_address,
-                subject, text, self.mlist.preferred_language)
+            msg = OwnerNotification(
+                self.mlist, subject, text, self.mlist.owners)
             msg.send(self.mlist)
         # The workflow must stop running here
         raise StopIteration
@@ -575,7 +573,7 @@ def _handle_confirmation_needed_events(event, template_name):
         ))
     msg = UserNotification(
         email_address, confirm_address, subject, text,
-        event.mlist.preferred_language)
+        event.mlist.preferred_language, role='subscriber')
     msg.send(event.mlist, add_precedence=False)
 
 
diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py
index c95f5ae9d..8fb216861 100644
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -160,6 +160,8 @@ class TestUnsubscription(unittest.TestCase):
         # When unsubscriptions must be approved by the moderator, but the
         # moderator defers this decision.
         user_manager = getUtility(IUserManager)
+        owner = user_manager.create_address('owner@example.com')
+        self._mlist.subscribe(owner, MemberRole.owner)
         anne = user_manager.create_address('anne@example.org', 'Anne Person')
         token, token_owner, member = self._manager.register(
             anne, pre_verified=True, pre_confirmed=True, pre_approved=True)
@@ -179,7 +181,7 @@ class TestUnsubscription(unittest.TestCase):
         else:
             raise AssertionError('No moderator email found')
         self.assertEqual(
-            item.msgdata['recipients'], {'test-owner@example.com'})
+            item.msgdata['recipients'], {'owner@example.com'})
         self.assertEqual(
             item.msg['subject'],
             'New unsubscription request from Test by anne@example.org')
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index b75c68542..468b5c487 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -439,6 +439,8 @@ class TestSubscriptionWorkflow(unittest.TestCase):
         bart = self._user_manager.create_user('bart@example.com', 'Bart User')
         address = set_preferred(bart)
         self._mlist.subscribe(address, MemberRole.moderator)
+        owner = self._user_manager.create_address('owner@example.com')
+        self._mlist.subscribe(owner, MemberRole.owner)
         workflow = SubscriptionWorkflow(self._mlist, anne,
                                         pre_verified=True,
                                         pre_confirmed=True)
@@ -452,9 +454,9 @@ class TestSubscriptionWorkflow(unittest.TestCase):
         else:
             raise AssertionError('No moderator email found')
         self.assertEqual(
-            item.msgdata['recipients'], {'test-owner@example.com'})
+            item.msgdata['recipients'], {'owner@example.com'})
         message = items[0].msg
-        self.assertEqual(message['From'], 'test-owner@example.com')
+        self.assertEqual(message['From'], 'noreply@example.com')
         self.assertEqual(message['To'], 'test-owner@example.com')
         self.assertEqual(
             message['Subject'],
diff --git a/src/mailman/app/tests/test_unsubscriptions.py b/src/mailman/app/tests/test_unsubscriptions.py
index dbd4cbe14..92175b090 100644
--- a/src/mailman/app/tests/test_unsubscriptions.py
+++ b/src/mailman/app/tests/test_unsubscriptions.py
@@ -23,6 +23,7 @@ from contextlib import suppress
 from mailman.app.lifecycle import create_list
 from mailman.app.subscriptions import UnSubscriptionWorkflow
 from mailman.interfaces.mailinglist import SubscriptionPolicy
+from mailman.interfaces.member import MemberRole
 from mailman.interfaces.pending import IPendings
 from mailman.interfaces.subscriptions import TokenOwner
 from mailman.interfaces.usermanager import IUserManager
@@ -278,13 +279,17 @@ class TestUnSubscriptionWorkflow(unittest.TestCase):
         # is so configured, a notification is sent to the list moderators.
         self._mlist.admin_immed_notify = True
         self._mlist.unsubscription_policy = SubscriptionPolicy.moderate
+        owner = self._user_manager.create_address('owner@example.com')
+        self._mlist.subscribe(owner, MemberRole.owner)
         workflow = UnSubscriptionWorkflow(
             self._mlist, self.anne, pre_confirmed=True)
         # Consume the entire state machine.
         list(workflow)
         items = get_queue_messages('virgin', expected_count=1)
         message = items[0].msg
-        self.assertEqual(message['From'], 'test-owner@example.com')
+        msgdata = items[0].msgdata
+        self.assertEqual(msgdata['recipients'], {'owner@example.com'})
+        self.assertEqual(message['From'], 'noreply@example.com')
         self.assertEqual(message['To'], 'test-owner@example.com')
         self.assertEqual(
             message['Subject'],
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index fec0303b0..6d1f89a98 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -27,7 +27,7 @@ from mailman.app.replybot import can_acknowledge
 from mailman.chains.base import TerminalChainBase
 from mailman.config import config
 from mailman.core.i18n import _, format_reasons
-from mailman.email.message import UserNotification
+from mailman.email.message import OwnerNotification, UserNotification
 from mailman.interfaces.autorespond import IAutoResponseSet, Response
 from mailman.interfaces.chain import HoldEvent
 from mailman.interfaces.languages import ILanguageManager
@@ -115,7 +115,7 @@ def autorespond_to_sender(mlist, sender, language=None):
             msg = UserNotification(
                 sender, mlist.owner_address,
                 _('Last autoresponse notification for today'),
-                text, lang=language)
+                text, lang=language, role='poster')
         msg.send(mlist)
         return False
     else:
@@ -206,7 +206,7 @@ class HoldChain(TerminalChainBase):
             adminaddr = mlist.bounces_address
             nmsg = UserNotification(
                 msg.sender, adminaddr, subject, text,
-                getUtility(ILanguageManager)[send_language_code])
+                getUtility(ILanguageManager)[send_language_code], 'poster')
             nmsg.send(mlist)
         # Now the message for the list moderators.  This one should appear to
         # come from <list>-owner since we really don't need to do bounce
@@ -225,9 +225,8 @@ class HoldChain(TerminalChainBase):
                 subject = _(
                     '$mlist.fqdn_listname post from $msg.sender requires '
                     'approval')
-                nmsg = UserNotification(mlist.owner_address,
-                                        mlist.owner_address,
-                                        subject, lang=language)
+                nmsg = OwnerNotification(
+                    mlist, subject, roster=mlist.owners)
                 nmsg.set_type('multipart/mixed')
                 template = getUtility(ITemplateLoader).get(
                     'list:admin:action:post', mlist)
diff --git a/src/mailman/chains/tests/test_hold.py b/src/mailman/chains/tests/test_hold.py
index a780585a3..900c24e52 100644
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -79,6 +79,8 @@ Content-Transfer-Encoding: 7bit
 Subject: Last autoresponse notification for today
 From: test-owner@example.com
 To: anne@example.com
+List-Administrivia: poster
+List-Id: <test.example.com>
 Precedence: bulk
 
 We have received a message from your address <anne@example.com>
@@ -125,6 +127,8 @@ A message body.
             if item.msg['to'] == 'test-owner@example.com':
                 part = item.msg.get_payload(0)
                 payloads['owner'] = part.get_payload().splitlines()
+                self.assertIn('List-Id', item.msg)
+                self.assertIn('List-Administrivia', item.msg)
             elif item.msg['To'] == 'anne@example.com':
                 payloads['sender'] = item.msg.get_payload().splitlines()
             else:
@@ -173,6 +177,8 @@ A message body.
         bart = self._user_manager.create_user('bart@example.com', 'Bart User')
         address = set_preferred(bart)
         self._mlist.subscribe(address, MemberRole.moderator)
+        owner = getUtility(IUserManager).create_address('owner@example.com')
+        self._mlist.subscribe(owner, MemberRole.owner)
         path = resource_filename('mailman.chains.tests', 'issue144.eml')
         with open(path, 'rb') as fp:
             msg = mfb(fp.read())
@@ -182,10 +188,11 @@ A message body.
         # delivery to the moderators.
         items = get_queue_messages('virgin', expected_count=1)
         msgdata = items[0].msgdata
+        msg = items[0].msg
         # Should get sent to -owner address.
-        self.assertEqual(msgdata['recipients'], {'test-owner@example.com'})
+        self.assertEqual(msg['to'], 'test-owner@example.com')
+        self.assertEqual(msgdata['recipients'], {'owner@example.com'})
         # Ensure that the subject looks correct in the postauth.txt.
-        msg = items[0].msg
         value = None
         for line in msg.get_payload(0).get_payload().splitlines():
             if line.strip().startswith('Subject:'):
diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py
index f8fa8d800..9ba3c85e9 100644
--- a/src/mailman/commands/cli_lists.py
+++ b/src/mailman/commands/cli_lists.py
@@ -24,7 +24,7 @@ from mailman.app.lifecycle import create_list, remove_list
 from mailman.core.constants import system_preferences
 from mailman.core.i18n import _
 from mailman.database.transaction import transaction
-from mailman.email.message import UserNotification
+from mailman.email.message import OwnerNotification
 from mailman.interfaces.address import (
     IEmailValidator, InvalidEmailAddressError)
 from mailman.interfaces.command import ICLISubCommand
@@ -208,10 +208,9 @@ def create(ctx, language, owners, notify, quiet, create_domain, fqdn_listname):
         # will match the template language.  Stashing and restoring the old
         # translation context is just (healthy? :) paranoia.
         with _.using(mlist.preferred_language.code):
-            msg = UserNotification(
-                owners, mlist.no_reply_address,
-                _('Your new mailing list: $fqdn_listname'),
-                text, mlist.preferred_language)
+            msg = OwnerNotification(
+                mlist, _('Your new mailing list: $fqdn_listname'),
+                text, mlist.owners)
             msg.send(mlist)
 
 
diff --git a/src/mailman/commands/docs/create.rst b/src/mailman/commands/docs/create.rst
index c80ff7a3d..a7855a557 100644
--- a/src/mailman/commands/docs/create.rst
+++ b/src/mailman/commands/docs/create.rst
@@ -128,7 +128,8 @@ The notification message is in the virgin queue.
     ...
     Subject: Your new mailing list: fly@example.com
     From: noreply@example.com
-    To: anne@example.com, bart@example.com, cate@example.com
+    List-Administrivia: list-owner
+    To: fly-owner@example.com
     ...
     <BLANKLINE>
     The mailing list 'fly@example.com' has just been created for you.
diff --git a/src/mailman/core/docs/chains.rst b/src/mailman/core/docs/chains.rst
index 84b340775..9c81cf5fc 100644
--- a/src/mailman/core/docs/chains.rst
+++ b/src/mailman/core/docs/chains.rst
@@ -122,7 +122,8 @@ This one is addressed to the list moderators.
 
     >>> print(messages[0].as_string())
     Subject: test@example.com post from aperson@example.com requires approval
-    From: test-owner@example.com
+    From: noreply@example.com
+    List-Administrivia: list-owner
     To: test-owner@example.com
     MIME-Version: 1.0
     ...
diff --git a/src/mailman/email/message.py b/src/mailman/email/message.py
index cdf5869c8..6b26ac749 100644
--- a/src/mailman/email/message.py
+++ b/src/mailman/email/message.py
@@ -31,6 +31,7 @@ from email.header import Header
 from email.mime.multipart import MIMEMultipart
 from mailman.config import config
 from mailman.interfaces.address import IEmailValidator
+from mailman.utilities.email import make_listid
 from public import public
 from zope.component import getUtility
 
@@ -132,7 +133,11 @@ class MultipartDigestMessage(MIMEMultipart, Message):
 class UserNotification(Message):
     """Class for internally crafted messages."""
 
-    def __init__(self, recipients, sender, subject=None, text=None, lang=None):
+    ROLES = ('poster', 'subscriber', 'moderator', 'list-owner',
+             'domain-owner', 'hostmaster')
+
+    def __init__(self, recipients, sender, subject=None, text=None, lang=None,
+                 role='subscriber'):
         Message.__init__(self)
         charset = (lang.charset if lang is not None else 'us-ascii')
         subject = ('(no subject)' if subject is None else subject)
@@ -147,6 +152,9 @@ class UserNotification(Message):
         else:
             self['To'] = recipients
             self.recipients = set([recipients])
+        if role not in self.ROLES:
+            raise ValueError('Unknown role: {}'.format(role))
+        self['List-Administrivia'] = role
 
     def send(self, mlist, *, add_precedence=True, **_kws):
         """Sends the message by enqueuing it to the 'virgin' queue.
@@ -170,6 +178,9 @@ class UserNotification(Message):
         # Ditto for Date: as required by RFC 2822.
         if 'date' not in self:
             self['Date'] = email.utils.formatdate(localtime=True)
+        # Also set the List-Id header
+        if 'list-id' not in self:
+            self['List-Id'] = make_listid(mlist)
         # UserNotifications are typically for admin messages, and for messages
         # other than list explosions.  Send these out as Precedence: bulk, but
         # don't override an existing Precedence: header.
@@ -205,7 +216,7 @@ class OwnerNotification(UserNotification):
             to = mlist.owner_address
         sender = config.mailman.site_owner
         UserNotification.__init__(self, recipients, sender, subject,
-                                  text, mlist.preferred_language)
+                                  text, mlist.preferred_language, 'list-owner')
         # Hack the To header to look like it's going to the -owner address
         del self['to']
         self['To'] = to
diff --git a/src/mailman/handlers/acknowledge.py b/src/mailman/handlers/acknowledge.py
index a8d8781fd..3e7032dcf 100644
--- a/src/mailman/handlers/acknowledge.py
+++ b/src/mailman/handlers/acknowledge.py
@@ -74,5 +74,5 @@ class Acknowledge:
         # queue.
         subject = _('$display_name post acknowledgment')
         usermsg = UserNotification(sender, mlist.bounces_address,
-                                   subject, text, language)
+                                   subject, text, language, 'poster')
         usermsg.send(mlist)
diff --git a/src/mailman/handlers/docs/replybot.rst b/src/mailman/handlers/docs/replybot.rst
index 9e18ce911..1053a9dfb 100644
--- a/src/mailman/handlers/docs/replybot.rst
+++ b/src/mailman/handlers/docs/replybot.rst
@@ -62,6 +62,7 @@ response.
     Subject: Auto-response for your message to the "XTest" mailing list
     From: _xtest-bounces@example.com
     To: aperson@example.com
+    List-Administrivia: poster
     X-Mailer: The Mailman Replybot
     X-Ack: No
     Message-ID: <...>
@@ -154,6 +155,7 @@ Unless the ``X-Ack:`` header has a value of ``yes``, in which case, the
     Subject: Auto-response for your message to the "XTest" mailing list
     From: _xtest-bounces@example.com
     To: asystem@example.com
+    List-Administrivia: poster
     X-Mailer: The Mailman Replybot
     X-Ack: No
     Message-ID: <...>
@@ -193,6 +195,7 @@ will get auto-responses: those sent to the ``-request`` address...
     Subject: Auto-response for your message to the "XTest" mailing list
     From: _xtest-bounces@example.com
     To: aperson@example.com
+    List-Administrivia: poster
     X-Mailer: The Mailman Replybot
     X-Ack: No
     Message-ID: <...>
@@ -226,6 +229,7 @@ will get auto-responses: those sent to the ``-request`` address...
     Subject: Auto-response for your message to the "XTest" mailing list
     From: _xtest-bounces@example.com
     To: aperson@example.com
+    List-Administrivia: poster
     X-Mailer: The Mailman Replybot
     X-Ack: No
     Message-ID: <...>
diff --git a/src/mailman/handlers/replybot.py b/src/mailman/handlers/replybot.py
index 62ceed8f5..5f918a2d9 100644
--- a/src/mailman/handlers/replybot.py
+++ b/src/mailman/handlers/replybot.py
@@ -105,8 +105,9 @@ class Replybot:
             )
         # Interpolation and Wrap the response text.
         text = wrap(expand(response_text, mlist, d))
-        outmsg = UserNotification(msg.sender, mlist.bounces_address,
-                                  subject, text, mlist.preferred_language)
+        outmsg = UserNotification(
+            msg.sender, mlist.bounces_address, subject, text,
+            mlist.preferred_language, 'poster')
         outmsg['X-Mailer'] = _('The Mailman Replybot')
         # prevent recursions and mail loops!
         outmsg['X-Ack'] = 'No'
diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py
index 63daab2f2..9ce0f51ba 100644
--- a/src/mailman/handlers/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -19,12 +19,11 @@
 
 import logging
 
-from email.utils import formataddr
 from mailman.core.i18n import _
-from mailman.handlers.cook_headers import uheader
 from mailman.interfaces.archiver import ArchivePolicy
 from mailman.interfaces.handler import IHandler
 from mailman.interfaces.mailinglist import IListArchiverSet
+from mailman.utilities.email import make_listid
 from public import public
 from zope.interface import implementer
 
@@ -41,18 +40,9 @@ def process(mlist, msg, msgdata):
     # headers by default, pissing off their users.  Too bad.  Fix the MUAs.
     if not mlist.include_rfc2369_headers:
         return
-    list_id = '{0.list_name}.{0.mail_host}'.format(mlist)
-    if mlist.description:
-        # Don't wrap the header since here we just want to get it properly RFC
-        # 2047 encoded.
-        i18ndesc = uheader(mlist, mlist.description, 'List-Id', maxlinelen=998)
-        listid_h = formataddr((str(i18ndesc), list_id))
-    else:
-        # Without a description, we need to ensure the MUST brackets.
-        listid_h = '<{}>'.format(list_id)
     # No other agent should add a List-ID header except Mailman.
     del msg['list-id']
-    msg['List-Id'] = listid_h
+    msg['List-Id'] = make_listid(mlist)
     # For internally crafted messages, we also add a (nonstandard),
     # "X-List-Administrivia: yes" header.  For all others (i.e. those coming
     # from list posts), we add a bunch of other RFC 2369 headers.
diff --git a/src/mailman/runners/command.py b/src/mailman/runners/command.py
index e885b217d..4a4e71113 100644
--- a/src/mailman/runners/command.py
+++ b/src/mailman/runners/command.py
@@ -223,7 +223,7 @@ class CommandRunner(Runner):
         language = getUtility(ILanguageManager)[msgdata['lang']]
         reply = UserNotification(msg.sender, mlist.bounces_address,
                                  _('The results of your email commands'),
-                                 lang=language)
+                                 lang=language, role='poster')
         cte = msg.get('content-transfer-encoding')
         if cte is not None:
             reply['Content-Transfer-Encoding'] = cte
diff --git a/src/mailman/runners/digest.py b/src/mailman/runners/digest.py
index dc5f65cbe..9bd636fc8 100644
--- a/src/mailman/runners/digest.py
+++ b/src/mailman/runners/digest.py
@@ -34,6 +34,7 @@ from mailman.email.message import Message, MultipartDigestMessage
 from mailman.handlers.decorate import decorate
 from mailman.interfaces.member import DeliveryMode, DeliveryStatus
 from mailman.interfaces.template import ITemplateLoader
+from mailman.utilities.email import make_listid
 from mailman.utilities.mailbox import Mailbox
 from mailman.utilities.string import expand, oneline, wrap
 from public import public
@@ -63,6 +64,7 @@ class Digester:
         self._message['Reply-To'] = mlist.posting_address
         self._message['Date'] = formatdate(localtime=True)
         self._message['Message-ID'] = make_msgid()
+        self._message['List-Id'] = make_listid(self._mlist)
         # In the rfc1153 digest, the masthead contains the digest boilerplate
         # plus any digest header.  In the MIME digests, the masthead and
         # digest header are separate MIME subobjects.  In either case, it's
diff --git a/src/mailman/runners/tests/test_digest.py b/src/mailman/runners/tests/test_digest.py
index 251e9cb02..8c1e0ebf1 100644
--- a/src/mailman/runners/tests/test_digest.py
+++ b/src/mailman/runners/tests/test_digest.py
@@ -66,6 +66,7 @@ class TestDigest(unittest.TestCase):
         for item in items:
             self.assertEqual(item.msg['subject'],
                              'Test Digest, Vol 1, Issue 1')
+            self.assertEqual(item.msg['list-id'], '<test.example.com>')
 
     def test_simple_message(self):
         # Subscribe some users receiving digests.
diff --git a/src/mailman/utilities/email.py b/src/mailman/utilities/email.py
index 81c2cb1fb..c8da62269 100644
--- a/src/mailman/utilities/email.py
+++ b/src/mailman/utilities/email.py
@@ -18,7 +18,9 @@
 """Email helpers."""
 
 from base64 import b32encode
+from email.utils import formataddr
 from hashlib import sha1
+from mailman.handlers.cook_headers import uheader
 from public import public
 
 
@@ -75,3 +77,16 @@ def add_message_hash(msg):
     del msg['x-message-id-hash']
     msg['X-Message-ID-Hash'] = hash32
     return hash32
+
+
+def make_listid(mlist):
+    list_id = '{0.list_name}.{0.mail_host}'.format(mlist)
+    if mlist.description:
+        # Don't wrap the header since here we just want to get it properly RFC
+        # 2047 encoded.
+        i18ndesc = uheader(mlist, mlist.description, 'List-Id', maxlinelen=998)
+        listid_h = formataddr((str(i18ndesc), list_id))
+    else:
+        # Without a description, we need to ensure the MUST brackets.
+        listid_h = '<{}>'.format(list_id)
+    return listid_h
diff --git a/src/mailman/utilities/tests/test_email.py b/src/mailman/utilities/tests/test_email.py
index b14b23306..3e3afde40 100644
--- a/src/mailman/utilities/tests/test_email.py
+++ b/src/mailman/utilities/tests/test_email.py
@@ -19,11 +19,12 @@
 
 import unittest
 
+from mailman.app.lifecycle import create_list
 from mailman.interfaces.messages import IMessageStore
 from mailman.testing.helpers import (
     specialized_message_from_string as mfs)
 from mailman.testing.layers import ConfigLayer
-from mailman.utilities.email import add_message_hash, split_email
+from mailman.utilities.email import add_message_hash, make_listid, split_email
 from zope.component import getUtility
 
 
@@ -149,3 +150,26 @@ X-Message-ID-Hash: abc
         self.assertEqual(len(x_message_id_hashes), 1)
         self.assertEqual(x_message_id_hashes[0],
                          'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
+
+
+class TestListID(unittest.TestCase):
+
+    layer = ConfigLayer
+
+    def setUp(self):
+        self._mlist = create_list('list@example.com')
+
+    def test_basic(self):
+        self.assertEqual(make_listid(self._mlist), '<list.example.com>')
+
+    def test_description(self):
+        self._mlist.description = 'List description'
+        self.assertEqual(make_listid(self._mlist),
+                         'List description <list.example.com>')
+
+    def test_i18n_description(self):
+        self._mlist.description = 'Description with non-ascii chars é ç à'
+        self.assertEqual(
+            make_listid(self._mlist),
+            '=?utf-8?q?Description_with_non-ascii_chars_=C3=A9_=C3=A7_=C3=A0?='
+            ' <list.example.com>')