aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Mailman/Bouncers/DSN.py3
-rw-r--r--Mailman/Cgi/confirm.py24
-rw-r--r--Mailman/Cgi/create.py7
-rw-r--r--Mailman/Cgi/options.py2
-rw-r--r--Mailman/Cgi/rmlist.py7
-rw-r--r--Mailman/Cgi/roster.py4
-rw-r--r--Mailman/Gui/Privacy.py15
-rw-r--r--Mailman/Handlers/Scrubber.py3
-rw-r--r--Mailman/Handlers/SpamDetect.py3
-rw-r--r--Mailman/MailList.py1
-rw-r--r--Mailman/Version.py2
-rw-r--r--Mailman/versions.py1
-rw-r--r--NEWS23
-rw-r--r--tests/bounces/dsn_18.txt52
-rw-r--r--tests/test_bounces.py1
15 files changed, 137 insertions, 11 deletions
diff --git a/Mailman/Bouncers/DSN.py b/Mailman/Bouncers/DSN.py
index 701617db..b316c696 100644
--- a/Mailman/Bouncers/DSN.py
+++ b/Mailman/Bouncers/DSN.py
@@ -56,7 +56,8 @@ def check(msg):
# Some MTAs have been observed that put comments on the action.
if action.startswith('delayed'):
return Stop
- if not action.startswith('fail'):
+ # opensmtpd uses non-compliant Action: error.
+ if not (action.startswith('fail') or action.startswith('error')):
# Some non-permanent failure, so ignore this block
continue
params = []
diff --git a/Mailman/Cgi/confirm.py b/Mailman/Cgi/confirm.py
index 8dd39aff..1175b81a 100644
--- a/Mailman/Cgi/confirm.py
+++ b/Mailman/Cgi/confirm.py
@@ -327,6 +327,12 @@ def subscription_cancel(mlist, doc, cookie):
try:
# Discard this cookie
userdesc = mlist.pend_confirm(cookie)[1]
+ except TypeError:
+ # See comment about TypeError in subscription_confirm.
+ # Give a generic message. It doesn't much matter what since it's a
+ # bot anyway.
+ doc.AddItem(_('Error'))
+ return
finally:
mlist.Unlock()
lang = userdesc.language
@@ -362,6 +368,10 @@ def subscription_confirm(mlist, doc, cookie, cgidata):
else:
digest = None
userdesc = mlist.pend_confirm(cookie, expunge=False)[1]
+ # There is a potential race condition if two (robotic?) clients try
+ # to confirm the same token simultaneously. If they both succeed in
+ # retrieving the data above, when the second gets here, the cookie
+ # is gone and TypeError is thrown. Catch it below.
fullname = cgidata.getfirst('realname', None)
if fullname is not None:
fullname = Utils.canonstr(fullname, lang)
@@ -379,7 +389,7 @@ def subscription_confirm(mlist, doc, cookie, cgidata):
the list moderator before you will be subscribed. Your request
has been forwarded to the list moderator, and you will be notified
of the moderator's decision."""))
- except Errors.NotAMemberError:
+ except (Errors.NotAMemberError, TypeError):
bad_confirmation(doc, _('''Invalid confirmation string. It is
possible that you are attempting to confirm a request for an
address that has already been unsubscribed.'''))
@@ -444,7 +454,8 @@ def unsubscription_confirm(mlist, doc, cookie):
i18n.set_language(lang)
doc.set_language(lang)
op, addr = mlist.ProcessConfirmation(cookie)
- except Errors.NotAMemberError:
+ # See comment about TypeError in subscription_confirm.
+ except (Errors.NotAMemberError, TypeError):
bad_confirmation(doc, _('''Invalid confirmation string. It is
possible that you are attempting to confirm a request for an
address that has already been unsubscribed.'''))
@@ -533,7 +544,8 @@ def addrchange_confirm(mlist, doc, cookie):
i18n.set_language(lang)
doc.set_language(lang)
op, oldaddr, newaddr = mlist.ProcessConfirmation(cookie)
- except Errors.NotAMemberError:
+ # See comment about TypeError in subscription_confirm.
+ except (Errors.NotAMemberError, TypeError):
bad_confirmation(doc, _('''Invalid confirmation string. It is
possible that you are attempting to confirm a request for an
address that has already been unsubscribed.'''))
@@ -657,7 +669,8 @@ def heldmsg_confirm(mlist, doc, cookie):
# Discard the message
mlist.HandleRequest(id, mm_cfg.DISCARD,
_('Sender discarded message via web.'))
- except (Errors.LostHeldMessage, KeyError):
+ # See comment about TypeError in subscription_confirm.
+ except (Errors.LostHeldMessage, KeyError, TypeError):
bad_confirmation(doc, _('''The held message with the Subject:
header <em>%(subject)s</em> could not be found. The most likely
reason for this is that the list moderator has already approved or
@@ -770,7 +783,8 @@ def reenable_confirm(mlist, doc, cookie):
i18n.set_language(lang)
doc.set_language(lang)
op, addr = mlist.ProcessConfirmation(cookie)
- except Errors.NotAMemberError:
+ # See comment about TypeError in subscription_confirm.
+ except (Errors.NotAMemberError, TypeError):
bad_confirmation(doc, _('''Invalid confirmation string. It is
possible that you are attempting to confirm a request for an
address that has already been unsubscribed.'''))
diff --git a/Mailman/Cgi/create.py b/Mailman/Cgi/create.py
index ebb211ae..d72e6967 100644
--- a/Mailman/Cgi/create.py
+++ b/Mailman/Cgi/create.py
@@ -162,6 +162,13 @@ def process_request(doc, cgidata):
if not ok:
ok = Utils.check_global_password(auth)
if not ok:
+ remote = os.environ.get('HTTP_FORWARDED_FOR',
+ os.environ.get('HTTP_X_FORWARDED_FOR',
+ os.environ.get('REMOTE_ADDR',
+ 'unidentified origin')))
+ syslog('security',
+ 'Authorization failed (create): list=%s: remote=%s',
+ listname, remote)
request_creation(
doc, cgidata,
_('You are not authorized to create new mailing lists'))
diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py
index 34a7718e..3a3b7841 100644
--- a/Mailman/Cgi/options.py
+++ b/Mailman/Cgi/options.py
@@ -296,7 +296,7 @@ def main():
os.environ.get('REMOTE_ADDR',
'unidentified origin')))
syslog('security',
- 'Authorization failed (private): user=%s: list=%s: remote=%s',
+ 'Authorization failed (options): user=%s: list=%s: remote=%s',
user, listname, remote)
# So as not to allow membership leakage, prompt for the email
# address and the password here.
diff --git a/Mailman/Cgi/rmlist.py b/Mailman/Cgi/rmlist.py
index 4472c1c5..4c37a15d 100644
--- a/Mailman/Cgi/rmlist.py
+++ b/Mailman/Cgi/rmlist.py
@@ -127,6 +127,13 @@ def process_request(doc, cgidata, mlist):
mm_cfg.AuthListAdmin,
mm_cfg.AuthSiteAdmin),
password) == mm_cfg.UnAuthorized:
+ remote = os.environ.get('HTTP_FORWARDED_FOR',
+ os.environ.get('HTTP_X_FORWARDED_FOR',
+ os.environ.get('REMOTE_ADDR',
+ 'unidentified origin')))
+ syslog('security',
+ 'Authorization failed (rmlist): list=%s: remote=%s',
+ mlist.internal_name(), remote)
request_deletion(
doc, mlist,
_('You are not authorized to delete this mailing list'))
diff --git a/Mailman/Cgi/roster.py b/Mailman/Cgi/roster.py
index abf87e08..eddd697b 100644
--- a/Mailman/Cgi/roster.py
+++ b/Mailman/Cgi/roster.py
@@ -123,8 +123,8 @@ def main():
os.environ.get('REMOTE_ADDR',
'unidentified origin')))
syslog('security',
- 'Authorization failed (roster): list=%s: remote=%s',
- listname, remote)
+ 'Authorization failed (roster): user=%s: list=%s: remote=%s',
+ addr, listname, remote)
return
# The document and its language
diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py
index 4df63da1..04e1b4d9 100644
--- a/Mailman/Gui/Privacy.py
+++ b/Mailman/Gui/Privacy.py
@@ -356,6 +356,21 @@ class Privacy(GUIBase):
be sent to anyone who posts to this list from a domain
with a DMARC Reject%(quarantine)s Policy.""")),
+ ('dmarc_moderation_addresses', mm_cfg.EmailListEx, (10, WIDTH), 1,
+ _("""List of addresses (or regexps) whose posts should always apply
+ <a href="?VARHELP=privacy/sender/dmarc_moderation_action"
+ >dmarc_moderation_action</a>
+ regardless of any domain specific DMARC Policy."""),
+
+ _("""Postings from any of these addresses will automatically
+ apply any DMARC action mitigation. This can be utilized to
+ automatically wrap or munge postings from known addresses or
+ domains that might have policies rejecting external mail From:
+ themselves.
+
+ <p>Add member addresses one per line; start the line with a ^
+ character to designate a regular expression match.""")),
+
('dmarc_wrapped_message_text', mm_cfg.Text, (10, WIDTH), 1,
_("""If dmarc_moderation_action applies and is Wrap Message,
and this text is provided, the text will be placed in a
diff --git a/Mailman/Handlers/Scrubber.py b/Mailman/Handlers/Scrubber.py
index 429312be..97e443b7 100644
--- a/Mailman/Handlers/Scrubber.py
+++ b/Mailman/Handlers/Scrubber.py
@@ -87,6 +87,9 @@ def guess_extension(ctype, ext):
all = guess_all_extensions(ctype, strict=False)
if ext in all:
return ext
+ if ctype.lower == 'application/octet-stream':
+ # For this type, all[0] is '.obj'. '.bin' is better.
+ return '.bin'
return all and all[0]
diff --git a/Mailman/Handlers/SpamDetect.py b/Mailman/Handlers/SpamDetect.py
index 7e035184..cf41303f 100644
--- a/Mailman/Handlers/SpamDetect.py
+++ b/Mailman/Handlers/SpamDetect.py
@@ -109,7 +109,8 @@ def process(mlist, msg, msgdata):
msgdata['from_is_list'] = 0
dn, addr = parseaddr(msg.get('from'))
if addr and mlist.dmarc_moderation_action > 0:
- if Utils.IsDMARCProhibited(mlist, addr):
+ if (mlist.GetPattern(addr, mlist.dmarc_moderation_addresses) or
+ Utils.IsDMARCProhibited(mlist, addr)):
# Note that for dmarc_moderation_action, 0 = Accept,
# 1 = Munge, 2 = Wrap, 3 = Reject, 4 = Discard
if mlist.dmarc_moderation_action == 1:
diff --git a/Mailman/MailList.py b/Mailman/MailList.py
index 8e2518c5..d74978af 100644
--- a/Mailman/MailList.py
+++ b/Mailman/MailList.py
@@ -424,6 +424,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
self.dmarc_none_moderation_action = (
mm_cfg.DEFAULT_DMARC_NONE_MODERATION_ACTION)
self.dmarc_moderation_notice = ''
+ self.dmarc_moderation_addresses = []
self.dmarc_wrapped_message_text = (
mm_cfg.DEFAULT_DMARC_WRAPPED_MESSAGE_TEXT)
self.equivalent_domains = (
diff --git a/Mailman/Version.py b/Mailman/Version.py
index da704882..f607c126 100644
--- a/Mailman/Version.py
+++ b/Mailman/Version.py
@@ -37,7 +37,7 @@ HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) |
(REL_LEVEL << 4) | (REL_SERIAL << 0))
# config.pck schema version number
-DATA_FILE_VERSION = 110
+DATA_FILE_VERSION = 111
# qfile/*.db schema version number
QFILE_SCHEMA_VERSION = 3
diff --git a/Mailman/versions.py b/Mailman/versions.py
index 428bb0af..d317a46d 100644
--- a/Mailman/versions.py
+++ b/Mailman/versions.py
@@ -497,6 +497,7 @@ def NewVars(l):
add_only_if_missing('dmarc_none_moderation_action',
mm_cfg.DEFAULT_DMARC_NONE_MODERATION_ACTION)
add_only_if_missing('dmarc_moderation_notice', '')
+ add_only_if_missing('dmarc_moderation_addresses', [])
add_only_if_missing('dmarc_wrapped_message_text',
mm_cfg.DEFAULT_DMARC_WRAPPED_MESSAGE_TEXT)
add_only_if_missing('member_verbosity_threshold',
diff --git a/NEWS b/NEWS
index e7071dce..cfca39a2 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,29 @@ Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
Here is a history of user visible changes to Mailman.
+2.1.30 (xx-xxx.xxxx)
+
+ New Features
+
+ - Thanks to Jim Popovitch, there is now a dmarc_moderation_addresses
+ list setting that can be used to apply dmarc_moderation_action to mail
+ From: addresses listed or matching listed regexps. This can be used
+ to modify mail to addresses that don't accept external mail From:
+ themselves.
+
+ Bug Fixes and other patches
+
+ - Fixed the confirm CGI to catch a rare TypeError on simultaneous
+ confirmations of the same token. (LP: #1785854)
+
+ - Scrubbed application/octet-stream MIME parts will now be given a
+ .bin extension instead of .obj.
+
+ - Added bounce recognition for a non-compliant opensmtpd DSN with
+ Action: error. (LP: #1805137)
+
+ - Corrected and augmented some security log messages. (LP: #1810098)
+
2.1.29 (24-Jul-2018)
Bug Fixes
diff --git a/tests/bounces/dsn_18.txt b/tests/bounces/dsn_18.txt
new file mode 100644
index 00000000..f2988895
--- /dev/null
+++ b/tests/bounces/dsn_18.txt
@@ -0,0 +1,52 @@
+Return-Path: <>
+X-Original-To: list.name-bounces@domain.replaced
+Delivered-To: list.name-bounces@domain.replaced
+Received: from mx.domain.replaced (mx.domain.replaced [177.XXX.XXX.XXX])
+ by mailhost.domain.replaced (Postfix) with ESMTP id 6D6C71E8A3
+ for <list.name-bounces@domain.replaced>; Thu, 22 Nov 2018 17:56:08 -0200 (BRST)
+Received: from mx.domain.replaced (mx.domain.replaced [local])
+ by mx.domain.replaced (OpenSMTPD) with ESMTPA id cce9bd49
+ for <list.name-bounces@domain.replaced>;
+ Thu, 22 Nov 2018 17:56:08 -0200 (-02)
+Subject: Delivery status notification: error
+From: Mailer Daemon <MAILER-DAEMON@mx.domain.replaced>
+To: list.name-bounces@domain.replaced
+Date: Thu, 22 Nov 2018 17:56:08 -0200 (-02)
+MIME-Version: 1.0
+Content-Type: multipart/mixed;boundary="9950749020440539406/mx.domain.replaced"
+Message-ID: <f778c4a058120b20@mx.domain.replaced>
+
+This is a MIME-encapsulated message.
+
+--9950749020440539406/mx.domain.replaced
+Content-Description: Notification
+Content-Type: text/plain; charset=us-ascii
+
+ Hi!
+
+ This is the MAILER-DAEMON, please DO NOT REPLY to this email.
+
+ An error has occurred while attempting to deliver a message for
+ the following list of recipients:
+
+email@replaced.net: Domain does not exist
+
+ Below is a copy of the original message:
+
+--9950749020440539406/mx.domain.replaced
+Content-Description: Delivery Report
+Content-Type: message/delivery-status
+
+Reporting-MTA: dns; mx.domain.replaced
+
+Final-Recipient: rfc822; email@replaced.net
+Action: error
+Status: 5.0.0
+
+--9950749020440539406/mx.domain.replaced
+Content-Description: Message headers
+Content-Type: text/rfc822-headers
+
+[ORIGINAL MESSAGE]
+
+--9950749020440539406/mx.domain.replaced--
diff --git a/tests/test_bounces.py b/tests/test_bounces.py
index e9bcff17..2f814428 100644
--- a/tests/test_bounces.py
+++ b/tests/test_bounces.py
@@ -122,6 +122,7 @@ class BounceTest(unittest.TestCase):
('DSN', 'dsn_15.txt', ['userx@example.com']),
('DSN', 'dsn_16.txt', ['userx@example.com']),
('DSN', 'dsn_17.txt', Stop),
+ ('DSN', 'dsn_18.txt', ['email@replaced.net']),
# Microsoft Exchange
('Exchange', 'microsoft_01.txt', ['userx@example.COM']),
('Exchange', 'microsoft_02.txt', ['userx@example.COM']),