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: Precedence: bulk --... @@ -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: Precedence: bulk --... @@ -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: Precedence: bulk --... 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: Precedence: bulk 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 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 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 -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 -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 -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 -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: Precedence: bulk We have received a message from your address @@ -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 ... 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'], '') 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), '') + + def test_description(self): + self._mlist.description = 'List description' + self.assertEqual(make_listid(self._mlist), + 'List description ') + + 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?=' + ' ')