patch mailman to add a simple question-and-answer CAPTCHA
authorRalf Jung <post@ralfj.de>
Sun, 3 Jun 2018 18:37:35 +0000 (20:37 +0200)
committerRalf Jung <post@ralfj.de>
Sun, 3 Jun 2018 18:37:35 +0000 (20:37 +0200)
roles/base/tasks/main.yml
roles/email/files/mailman-patched/Captcha.py [new file with mode: 0644]
roles/email/files/mailman-patched/Cgi/listinfo.py [new file with mode: 0644]
roles/email/files/mailman-patched/Cgi/subscribe.py [new file with mode: 0644]
roles/email/files/mailman-patched/de/listinfo.html [new file with mode: 0644]
roles/email/files/mailman-patched/en/listinfo.html [new file with mode: 0644]
roles/email/tasks/mailman.yml
roles/email/templates/mm_cfg.py

index 0fefd93..d1c108c 100644 (file)
@@ -3,7 +3,7 @@
   when: not (ansible_distribution == "Debian" and ansible_lsb.major_release|int >= 9)
   command: "false"
 - name: detect if we have backports in the sources.list
-  command: fgrep backports /etc/apt/sources.list
+  command: 'fgrep backports /etc/apt/sources.list'
   register: backports
   failed_when: backports.rc == 2
   changed_when: False
diff --git a/roles/email/files/mailman-patched/Captcha.py b/roles/email/files/mailman-patched/Captcha.py
new file mode 100644 (file)
index 0000000..42f75f0
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright (C) 2018 by Ralf Jung
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+import random
+from Mailman import Utils
+
+def displayhtml(mlist, captchas):
+    """Returns a CAPTCHA question, the HTML for the answer box, and
+    the data to be put into the CSRF token"""
+    idx = random.randrange(len(captchas))
+    question = captchas[idx][0]
+    box_html = mlist.FormatBox('captcha_answer', size=30)
+    return (Utils.websafe(question), box_html, str(idx))
+
+def verify(idx, given_answer, captchas):
+    try:
+        idx = int(idx)
+    except ValueError:
+        return False
+    if not idx in range(len(captchas)):
+        return False
+    # Chec the given answer
+    correct_answers = captchas[idx][1]
+    given_answer = given_answer.strip().lower()
+    return given_answer in map(lambda x: x.strip().lower(), correct_answers)
diff --git a/roles/email/files/mailman-patched/Cgi/listinfo.py b/roles/email/files/mailman-patched/Cgi/listinfo.py
new file mode 100644 (file)
index 0000000..0ce6393
--- /dev/null
@@ -0,0 +1,265 @@
+# Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Produce listinfo page, primary web entry-point to mailing lists.
+"""
+
+# No lock needed in this script, because we don't change data.
+
+import os
+import cgi
+import time
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import Captcha # MAILMAN_CAPTCHA_PATCHED
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+
+def main():
+    parts = Utils.GetPathPieces()
+    if not parts:
+        listinfo_overview()
+        return
+
+    listname = parts[0].lower()
+    try:
+        mlist = MailList.MailList(listname, lock=0)
+    except Errors.MMListError, e:
+        # Avoid cross-site scripting attacks
+        safelistname = Utils.websafe(listname)
+        # Send this with a 404 status.
+        print 'Status: 404 Not Found'
+        listinfo_overview(_('No such list <em>%(safelistname)s</em>'))
+        syslog('error', 'listinfo: No such list "%s": %s', listname, e)
+        return
+
+    # See if the user want to see this page in other language
+    cgidata = cgi.FieldStorage()
+    try:
+        language = cgidata.getvalue('language')
+    except TypeError:
+        # Someone crafted a POST with a bad Content-Type:.
+        doc = Document()
+        doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+        doc.AddItem(Header(2, _("Error")))
+        doc.AddItem(Bold(_('Invalid options to CGI script.')))
+        # Send this with a 400 status.
+        print 'Status: 400 Bad Request'
+        print doc.Format()
+        return
+
+    if not Utils.IsLanguage(language):
+        language = mlist.preferred_language
+    i18n.set_language(language)
+    list_listinfo(mlist, language)
+
+
+
+
+def listinfo_overview(msg=''):
+    # Present the general listinfo overview
+    hostname = Utils.get_domain()
+    # Set up the document and assign it the correct language.  The only one we
+    # know about at the moment is the server's default.
+    doc = Document()
+    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+    legend = _("%(hostname)s Mailing Lists")
+    doc.SetTitle(legend)
+
+    table = Table(border=0, width="100%")
+    table.AddRow([Center(Header(2, legend))])
+    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+                      bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+    # Skip any mailing lists that isn't advertised.
+    advertised = []
+    listnames = Utils.list_names()
+    listnames.sort()
+
+    for name in listnames:
+        try:
+            mlist = MailList.MailList(name, lock=0)
+        except Errors.MMUnknownListError:
+            # The list could have been deleted by another process.
+            continue
+        if mlist.advertised:
+            if mm_cfg.VIRTUAL_HOST_OVERVIEW and (
+                   mlist.web_page_url.find('/%s/' % hostname) == -1 and
+                   mlist.web_page_url.find('/%s:' % hostname) == -1):
+                # List is for different identity of this host - skip it.
+                continue
+            else:
+                advertised.append((mlist.GetScriptURL('listinfo'),
+                                   mlist.real_name,
+                                   Utils.websafe(mlist.description)))
+    if msg:
+        greeting = FontAttr(msg, color="ff5060", size="+1")
+    else:
+        greeting = FontAttr(_('Welcome!'), size='+2')
+
+    welcome = [greeting]
+    mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
+    if not advertised:
+        welcome.extend(
+            _('''<p>There currently are no publicly-advertised
+            %(mailmanlink)s mailing lists on %(hostname)s.'''))
+    else:
+        welcome.append(
+            _('''<p>Below is a listing of all the public mailing lists on
+            %(hostname)s.  Click on a list name to get more information about
+            the list, or to subscribe, unsubscribe, and change the preferences
+            on your subscription.'''))
+
+    # set up some local variables
+    adj = msg and _('right') or ''
+    siteowner = Utils.get_site_email()
+    welcome.extend(
+        (_(''' To visit the general information page for an unadvertised list,
+        open a URL similar to this one, but with a '/' and the %(adj)s
+        list name appended.
+        <p>List administrators, you can visit '''),
+         Link(Utils.ScriptURL('admin'),
+              _('the list admin overview page')),
+         _(''' to find the management interface for your list.
+         <p>If you are having trouble using the lists, please contact '''),
+         Link('mailto:' + siteowner, siteowner),
+         '.<p>'))
+
+    table.AddRow([apply(Container, welcome)])
+    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
+
+    if advertised:
+        table.AddRow(['&nbsp;', '&nbsp;'])
+        table.AddRow([Bold(FontAttr(_('List'), size='+2')),
+                      Bold(FontAttr(_('Description'), size='+2'))
+                      ])
+        highlight = 1
+        for url, real_name, description in advertised:
+            table.AddRow(
+                [Link(url, Bold(real_name)),
+                      description or Italic(_('[no description available]'))])
+            if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
+                table.AddRowInfo(table.GetCurrentRowIndex(),
+                                 bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
+            highlight = not highlight
+
+    doc.AddItem(table)
+    doc.AddItem('<hr>')
+    doc.AddItem(MailmanLogo())
+    print doc.Format()
+
+
+
+
+def list_listinfo(mlist, lang):
+    # Generate list specific listinfo
+    doc = HeadlessDocument()
+    doc.set_language(lang)
+
+    replacements = mlist.GetStandardReplacements(lang)
+
+    if not mlist.digestable or not mlist.nondigestable:
+        replacements['<mm-digest-radio-button>'] = ""
+        replacements['<mm-undigest-radio-button>'] = ""
+        replacements['<mm-digest-question-start>'] = '<!-- '
+        replacements['<mm-digest-question-end>'] = ' -->'
+    else:
+        replacements['<mm-digest-radio-button>'] = mlist.FormatDigestButton()
+        replacements['<mm-undigest-radio-button>'] = \
+                                                   mlist.FormatUndigestButton()
+        replacements['<mm-digest-question-start>'] = ''
+        replacements['<mm-digest-question-end>'] = ''
+    replacements['<mm-plain-digests-button>'] = \
+                                              mlist.FormatPlainDigestsButton()
+    replacements['<mm-mime-digests-button>'] = mlist.FormatMimeDigestsButton()
+    replacements['<mm-subscribe-box>'] = mlist.FormatBox('email', size=30)
+    replacements['<mm-subscribe-button>'] = mlist.FormatButton(
+        'email-button', text=_('Subscribe'))
+    replacements['<mm-new-password-box>'] = mlist.FormatSecureBox('pw')
+    replacements['<mm-confirm-password>'] = mlist.FormatSecureBox('pw-conf')
+    replacements['<mm-subscribe-form-start>'] = mlist.FormatFormStart(
+        'subscribe')
+    if mm_cfg.SUBSCRIBE_FORM_SECRET:
+        now = str(int(time.time()))
+        remote = os.environ.get('HTTP_FORWARDED_FOR',
+                 os.environ.get('HTTP_X_FORWARDED_FOR',
+                 os.environ.get('REMOTE_ADDR',
+                                'w.x.y.z')))
+        # Try to accept a range in case of load balancers, etc.  (LP: #1447445)
+        if remote.find('.') >= 0:
+            # ipv4 - drop last octet
+            remote = remote.rsplit('.', 1)[0]
+        else:
+            # ipv6 - drop last 16 (could end with :: in which case we just
+            #        drop one : resulting in an invalid format, but it's only
+            #        for our hash so it doesn't matter.
+            remote = remote.rsplit(':', 1)[0]
+        # get CAPTCHA data
+        (captcha_question, captcha_box, captcha_idx) = Captcha.displayhtml(mlist, mm_cfg.CAPTCHAS)
+        replacements['<mm-captcha-question>'] = captcha_question
+        replacements['<mm-captcha-box>'] = captcha_box
+        # fill form
+        replacements['<mm-subscribe-form-start>'] += (
+                '<input type="hidden" name="sub_form_token" value="%s:%s:%s">\n'
+                % (now, captcha_idx, Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET +
+                          now +
+                          captcha_idx +
+                          mlist.internal_name() +
+                          remote
+                          ).hexdigest()
+                    )
+                )
+    # Roster form substitutions
+    replacements['<mm-roster-form-start>'] = mlist.FormatFormStart('roster')
+    replacements['<mm-roster-option>'] = mlist.FormatRosterOptionForUser(lang)
+    # Options form substitutions
+    replacements['<mm-options-form-start>'] = mlist.FormatFormStart('options')
+    replacements['<mm-editing-options>'] = mlist.FormatEditingOption(lang)
+    replacements['<mm-info-button>'] = SubmitButton('UserOptions',
+                                                    _('Edit Options')).Format()
+    # If only one language is enabled for this mailing list, omit the choice
+    # buttons.
+    if len(mlist.GetAvailableLanguages()) == 1:
+        displang = ''
+    else:
+        displang = mlist.FormatButton('displang-button',
+                                      text = _("View this page in"))
+    replacements['<mm-displang-box>'] = displang
+    replacements['<mm-lang-form-start>'] = mlist.FormatFormStart('listinfo')
+    replacements['<mm-fullname-box>'] = mlist.FormatBox('fullname', size=30)
+
+    # Do the expansion.
+    doc.AddItem(mlist.ParseTags('listinfo.html', replacements, lang))
+    print doc.Format()
+
+
+
+
+if __name__ == "__main__":
+    main()
diff --git a/roles/email/files/mailman-patched/Cgi/subscribe.py b/roles/email/files/mailman-patched/Cgi/subscribe.py
new file mode 100644 (file)
index 0000000..153286d
--- /dev/null
@@ -0,0 +1,333 @@
+# Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Process subscription or roster requests from listinfo form."""
+
+import sys
+import os
+import cgi
+import time
+import signal
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import Captcha # MAILMAN_CAPTCHA_PATCHED
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman import Message
+from Mailman.UserDesc import UserDesc
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+SLASH = '/'
+ERRORSEP = '\n\n<p>'
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+
+def main():
+    doc = Document()
+    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+    parts = Utils.GetPathPieces()
+    if not parts:
+        doc.AddItem(Header(2, _("Error")))
+        doc.AddItem(Bold(_('Invalid options to CGI script')))
+        print doc.Format()
+        return
+
+    listname = parts[0].lower()
+    try:
+        mlist = MailList.MailList(listname, lock=0)
+    except Errors.MMListError, e:
+        # Avoid cross-site scripting attacks
+        safelistname = Utils.websafe(listname)
+        doc.AddItem(Header(2, _("Error")))
+        doc.AddItem(Bold(_('No such list <em>%(safelistname)s</em>')))
+        # Send this with a 404 status.
+        print 'Status: 404 Not Found'
+        print doc.Format()
+        syslog('error', 'subscribe: No such list "%s": %s\n', listname, e)
+        return
+
+    # See if the form data has a preferred language set, in which case, use it
+    # for the results.  If not, use the list's preferred language.
+    cgidata = cgi.FieldStorage()
+    try:
+        language = cgidata.getvalue('language', '')
+    except TypeError:
+        # Someone crafted a POST with a bad Content-Type:.
+        doc.AddItem(Header(2, _("Error")))
+        doc.AddItem(Bold(_('Invalid options to CGI script.')))
+        # Send this with a 400 status.
+        print 'Status: 400 Bad Request'
+        print doc.Format()
+        return
+    if not Utils.IsLanguage(language):
+        language = mlist.preferred_language
+    i18n.set_language(language)
+    doc.set_language(language)
+
+    # We need a signal handler to catch the SIGTERM that can come from Apache
+    # when the user hits the browser's STOP button.  See the comment in
+    # admin.py for details.
+    #
+    # BAW: Strictly speaking, the list should not need to be locked just to
+    # read the request database.  However the request database asserts that
+    # the list is locked in order to load it and it's not worth complicating
+    # that logic.
+    def sigterm_handler(signum, frame, mlist=mlist):
+        # Make sure the list gets unlocked...
+        mlist.Unlock()
+        # ...and ensure we exit, otherwise race conditions could cause us to
+        # enter MailList.Save() while we're in the unlocked state, and that
+        # could be bad!
+        sys.exit(0)
+
+    mlist.Lock()
+    try:
+        # Install the emergency shutdown signal handler
+        signal.signal(signal.SIGTERM, sigterm_handler)
+
+        process_form(mlist, doc, cgidata, language)
+        mlist.Save()
+    finally:
+        mlist.Unlock()
+
+
+
+
+def process_form(mlist, doc, cgidata, lang):
+    listowner = mlist.GetOwnerEmail()
+    realname = mlist.real_name
+    results = []
+
+    # The email address being subscribed, required
+    email = cgidata.getvalue('email', '').strip()
+    if not email:
+        results.append(_('You must supply a valid email address.'))
+
+    fullname = cgidata.getvalue('fullname', '')
+    # Canonicalize the full name
+    fullname = Utils.canonstr(fullname, lang)
+    # Who was doing the subscribing?
+    remote = os.environ.get('HTTP_FORWARDED_FOR',
+             os.environ.get('HTTP_X_FORWARDED_FOR',
+             os.environ.get('REMOTE_ADDR',
+                            'unidentified origin')))
+    # Are we checking the hidden data?
+    if mm_cfg.SUBSCRIBE_FORM_SECRET:
+        now = int(time.time())
+        # Try to accept a range in case of load balancers, etc.  (LP: #1447445)
+        if remote.find('.') >= 0:
+            # ipv4 - drop last octet
+            remote1 = remote.rsplit('.', 1)[0]
+        else:
+            # ipv6 - drop last 16 (could end with :: in which case we just
+            #        drop one : resulting in an invalid format, but it's only
+            #        for our hash so it doesn't matter.
+            remote1 = remote.rsplit(':', 1)[0]
+        try:
+            ftime, fcaptcha_idx, fhash = cgidata.getvalue('sub_form_token', '').split(':')
+            then = int(ftime)
+        except ValueError:
+            ftime = fcaptcha_idx = fhash = ''
+            then = 0
+        token = Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET +
+                              ftime +
+                              fcaptcha_idx +
+                              mlist.internal_name() +
+                              remote1).hexdigest()
+        if ftime and now - then > mm_cfg.FORM_LIFETIME:
+            results.append(_('The form is too old.  Please GET it again.'))
+        if ftime and now - then < mm_cfg.SUBSCRIBE_FORM_MIN_TIME:
+            results.append(
+    _('Please take a few seconds to fill out the form before submitting it.'))
+        if ftime and token != fhash:
+            results.append(
+                _("The hidden token didn't match.  Did your IP change?"))
+        if not ftime:
+            results.append(
+    _('There was no hidden token in your submission or it was corrupted.'))
+            results.append(_('You must GET the form before submitting it.'))
+        # Check captcha
+        captcha_answer = cgidata.getvalue('captcha_answer', '')
+        if not Captcha.verify(fcaptcha_idx, captcha_answer, mm_cfg.CAPTCHAS):
+            results.append(_('This was not the right answer to the CAPTCHA question.'))
+    # Was an attempt made to subscribe the list to itself?
+    if email == mlist.GetListEmail():
+        syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote)
+        results.append(_('You may not subscribe a list to itself!'))
+    # If the user did not supply a password, generate one for him
+    password = cgidata.getvalue('pw', '').strip()
+    confirmed = cgidata.getvalue('pw-conf', '').strip()
+
+    if not password and not confirmed:
+        password = Utils.MakeRandomPassword()
+    elif not password or not confirmed:
+        results.append(_('If you supply a password, you must confirm it.'))
+    elif password <> confirmed:
+        results.append(_('Your passwords did not match.'))
+
+    # Get the digest option for the subscription.
+    digestflag = cgidata.getvalue('digest')
+    if digestflag:
+        try:
+            digest = int(digestflag)
+        except ValueError:
+            digest = 0
+    else:
+        digest = mlist.digest_is_default
+
+    # Sanity check based on list configuration.  BAW: It's actually bogus that
+    # the page allows you to set the digest flag if you don't really get the
+    # choice. :/
+    if not mlist.digestable:
+        digest = 0
+    elif not mlist.nondigestable:
+        digest = 1
+
+    if results:
+        print_results(mlist, ERRORSEP.join(results), doc, lang)
+        return
+
+    # If this list has private rosters, we have to be careful about the
+    # message that gets printed, otherwise the subscription process can be
+    # used to mine for list members.  It may be inefficient, but it's still
+    # possible, and that kind of defeats the purpose of private rosters.
+    # We'll use this string for all successful or unsuccessful subscription
+    # results.
+    if mlist.private_roster == 0:
+        # Public rosters
+        privacy_results = ''
+    else:
+        privacy_results = _("""\
+Your subscription request has been received, and will soon be acted upon.
+Depending on the configuration of this mailing list, your subscription request
+may have to be first confirmed by you via email, or approved by the list
+moderator.  If confirmation is required, you will soon get a confirmation
+email which contains further instructions.""")
+
+    try:
+        userdesc = UserDesc(email, fullname, password, digest, lang)
+        mlist.AddMember(userdesc, remote)
+        results = ''
+    # Check for all the errors that mlist.AddMember can throw options on the
+    # web page for this cgi
+    except Errors.MembershipIsBanned:
+        results = _("""The email address you supplied is banned from this
+        mailing list.  If you think this restriction is erroneous, please
+        contact the list owners at %(listowner)s.""")
+    except Errors.MMBadEmailError:
+        results = _("""\
+The email address you supplied is not valid.  (E.g. it must contain an
+`@'.)""")
+    except Errors.MMHostileAddress:
+        results = _("""\
+Your subscription is not allowed because the email address you gave is
+insecure.""")
+    except Errors.MMSubscribeNeedsConfirmation:
+        # Results string depends on whether we have private rosters or not
+        if privacy_results:
+            results = privacy_results
+        else:
+            results = _("""\
+Confirmation from your email address is required, to prevent anyone from
+subscribing you without permission.  Instructions are being sent to you at
+%(email)s.  Please note your subscription will not start until you confirm
+your subscription.""")
+    except Errors.MMNeedApproval, x:
+        # Results string depends on whether we have private rosters or not
+        if privacy_results:
+            results = privacy_results
+        else:
+            # We need to interpolate into x.__str__()
+            x = _(str(x))
+            results = _("""\
+Your subscription request was deferred because %(x)s.  Your request has been
+forwarded to the list moderator.  You will receive email informing you of the
+moderator's decision when they get to your request.""")
+    except Errors.MMAlreadyAMember:
+        # Results string depends on whether we have private rosters or not
+        if not privacy_results:
+            results = _('You are already subscribed.')
+        else:
+            results = privacy_results
+            # This could be a membership probe.  For safety, let the user know
+            # a probe occurred.  BAW: should we inform the list moderator?
+            listaddr = mlist.GetListEmail()
+            # Set the language for this email message to the member's language.
+            mlang = mlist.getMemberLanguage(email)
+            otrans = i18n.get_translation()
+            i18n.set_language(mlang)
+            try:
+                msg = Message.UserNotification(
+                    mlist.getMemberCPAddress(email),
+                    mlist.GetBouncesEmail(),
+                    _('Mailman privacy alert'),
+                    _("""\
+An attempt was made to subscribe your address to the mailing list
+%(listaddr)s.  You are already subscribed to this mailing list.
+
+Note that the list membership is not public, so it is possible that a bad
+person was trying to probe the list for its membership.  This would be a
+privacy violation if we let them do this, but we didn't.
+
+If you submitted the subscription request and forgot that you were already
+subscribed to the list, then you can ignore this message.  If you suspect that
+an attempt is being made to covertly discover whether you are a member of this
+list, and you are worried about your privacy, then feel free to send a message
+to the list administrator at %(listowner)s.
+"""), lang=mlang)
+            finally:
+                i18n.set_translation(otrans)
+            msg.send(mlist)
+    # These shouldn't happen unless someone's tampering with the form
+    except Errors.MMCantDigestError:
+        results = _('This list does not support digest delivery.')
+    except Errors.MMMustDigestError:
+        results = _('This list only supports digest delivery.')
+    else:
+        # Everything's cool.  Our return string actually depends on whether
+        # this list has private rosters or not
+        if privacy_results:
+            results = privacy_results
+        else:
+            results = _("""\
+You have been successfully subscribed to the %(realname)s mailing list.""")
+    # Show the results
+    print_results(mlist, results, doc, lang)
+
+
+
+
+def print_results(mlist, results, doc, lang):
+    # The bulk of the document will come from the options.html template, which
+    # includes its own html armor (head tags, etc.).  Suppress the head that
+    # Document() derived pages get automatically.
+    doc.suppress_head = 1
+
+    replacements = mlist.GetStandardReplacements(lang)
+    replacements['<mm-results>'] = results
+    output = mlist.ParseTags('subscribe.html', replacements, lang)
+    doc.AddItem(output)
+    print doc.Format()
diff --git a/roles/email/files/mailman-patched/de/listinfo.html b/roles/email/files/mailman-patched/de/listinfo.html
new file mode 100644 (file)
index 0000000..2657d0c
--- /dev/null
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!-- updated from Revision 1.1  -->
+<HTML>
+  <HEAD>
+    <TITLE><MM-List-Name> Infoseite</TITLE>
+
+  </HEAD>
+  <BODY BGCOLOR="#ffffff">
+
+    <P>
+      <TABLE BORDER="0" CELLSPACING="4" CELLPADDING="5">
+       <TR>
+         <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#99CCFF" ALIGN="CENTER">
+           <B><FONT COLOR="#000000" SIZE="+1"><MM-List-Name> --
+       <MM-List-Description></FONT></B>
+         </TD>
+       </TR>
+       <tr>
+           <td colspan="2">
+             <p>&nbsp;
+           </td>
+         </tr>
+         <tr>
+           <TD COLSPAN="1" WIDTH="100%" BGCOLOR="#FFF0D0">
+             <B><FONT COLOR="#000000">&Uuml;ber <MM-List-Name></FONT></B>
+           </TD>
+            <TD COLSPAN="1" WIDTH="100%" BGCOLOR="#FFF0D0">
+              <MM-lang-form-start><MM-displang-box> <MM-list-langs>
+              <MM-form-end>
+            </TD>
+         </TR>
+           <tr>
+             <td colspan="2">
+               <P><MM-List-Info></P>
+         <p> Um fr&uuml;here Nachrichten an diese Liste zu sehen,
+besuchen Sie bitte das <MM-Archive>Archiv der
+Liste <MM-List-Name></MM-Archive>. <MM-Restricted-List-Message>
+         </p>
+       </TD>
+      </TR>
+      <TR>
+       <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+         <B><FONT COLOR="#000000">Benutzung von <MM-List-Name></FONT></B>
+       </TD>
+      </TR>
+      <tr>
+       <td colspan="2">
+         Um eine Nachricht an alle Listenmitglieder zu senden, schicken Sie
+          diese an
+         <A HREF="mailto:<MM-Posting-Addr>"><MM-Posting-Addr></A>.
+
+         <p>Sie k&ouml;nnen im folgenden Abschnitt diese Liste abonnieren
+             oder ein bestehendes Abonnement &auml;ndern.
+       </td>
+      </tr>
+      <TR>
+       <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+         <B><FONT COLOR="#000000">Abonnieren von <MM-List-Name></FONT></B>
+       </TD>
+      </TR>
+      <tr>
+       <td colspan="2">
+         <P>
+           Abonnieren Sie <MM-List-Name>, indem Sie das folgende Formular
+ ausf&uuml;llen:
+         <MM-List-Subscription-Msg>
+         <ul>
+    <MM-Subscribe-Form-Start>
+             <TABLE BORDER="0" CELLSPACING="2" CELLPADDING="2"
+               WIDTH="70%">
+               <TR>
+                 <TD BGCOLOR="#dddddd" WIDTH="55%">Ihre E-Mail-Adresse:</TD>
+                 <TD WIDTH="33%"><MM-Subscribe-Box>
+       </TD>
+       <TD WIDTH="12%">&nbsp;</TD></TR>
+      <tr>
+        <td bgcolor="#dddddd" width="55%">Ihr Name (optional):</td>
+        <td width="33%"><mm-fullname-box></td>
+       <TD WIDTH="12%">&nbsp;</TD></TR>
+      <TR>
+       <TD COLSPAN="3"><FONT SIZE=-1>Sie k&ouml;nnen weiter unten ein Passwort
+           eingeben. Dieses Passwort bietet nur eine geringe Sicherheit,
+            sollte aber verhindern, dass andere Ihr Abonnement
+            manipulieren. <b>Verwenden Sie kein wertvolles Passwort</b>,
+            da es ab und zu an Sie geschickt wird und <b>im Klartext gespeichert</b> wird!
+
+            <br><br>Wenn Sie kein Passwort eingeben, wird f&uuml;r Sie ein
+            Zufallspasswort generiert und Ihnen zugeschickt, sobald Sie Ihr
+            Abonnement best&auml;tigt haben. Sie k&ouml;nnen sich Ihr Passwort
+            jederzeit per E-Mail zuschicken lassen, wenn Sie weiter unten
+            die Seite zum &auml;ndern Ihrer pers&ouml;nlichen Einstellungen aufrufen.
+           <MM-Reminder>       </font></TD>
+      </TR>
+      <TR>
+       <TD BGCOLOR="#dddddd">W&auml;hlen Sie ein Passwort:</TD>
+       <TD><MM-New-Password-Box></TD>
+       <TD>&nbsp;</TD></TR>
+      <TR>
+       <TD BGCOLOR="#dddddd">Erneute Eingabe zur Best&auml;tigung:</TD>
+       <TD><MM-Confirm-Password></TD>
+       <TD>&nbsp; </TD></TR>
+      <tr>
+        <TD BGCOLOR="#dddddd">Welche Sprache bevorzugen Sie zur
+ Benutzerf&uuml;hrung?</TD>
+        <TD> <MM-list-langs></TD>
+        <TD>&nbsp; </TD></TR>
+      <mm-digest-question-start>
+      <tr>
+       <td>M&ouml;chten Sie die Listenmails geb&uuml;ndelt in Form einer t&auml;glichen
+            Zusammenfassung (digest) erhalten?
+         </td>
+       <td><MM-Undigest-Radio-Button>Nein
+           <MM-Digest-Radio-Button>Ja
+       </TD>
+      </tr>
+      <mm-digest-question-end>
+      <tr>
+        <TD BGCOLOR="#dddddd">Bitte beantworten Sie die folgende Frage, um zu beweisen, dass Sie kein Bot sind:
+          <mm-captcha-question>
+        </TD>
+        <TD><mm-captcha-box></TD>
+      </tr>
+      <tr>
+       <td colspan="3">
+         <center><MM-Subscribe-Button></center>
+    </td>
+    </tr>
+    </TABLE>
+    <MM-Form-End>
+    </ul>
+    <TR>
+      <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+       <a name="subscribers">
+        <B><FONT COLOR="#000000">Abonnenten der Liste <MM-List-Name></FONT></B></a>
+      </TD>
+    </TR>
+    <tr>
+      <TD COLSPAN="2" WIDTH="100%">
+       <MM-Roster-Form-Start>
+       <MM-Roster-Option>
+           <MM-Form-End>
+       </TD>
+       </TR>
+    <TR>
+      <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+        <B><FONT COLOR="#000000">Austragen / &Auml;ndern einer Mailadresse</FONT></B>
+      </TD>
+    </TR>
+      <TD COLSPAN="2" WIDTH="100%">
+       <MM-Options-Form-Start>
+<br>
+       <MM-Editing-Options>
+<br>
+               <MM-Form-End>
+      </td>
+    </tr>
+  </table>
+<MM-Mailman-Footer>
+</BODY>
+</HTML>
diff --git a/roles/email/files/mailman-patched/en/listinfo.html b/roles/email/files/mailman-patched/en/listinfo.html
new file mode 100644 (file)
index 0000000..cf0eca6
--- /dev/null
@@ -0,0 +1,152 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!-- $Revision: 5865 $ -->
+<HTML>
+  <HEAD>
+    <TITLE><MM-List-Name> Info Page</TITLE>
+
+  </HEAD>
+  <BODY BGCOLOR="#ffffff">
+
+    <P>
+      <TABLE BORDER="0" CELLSPACING="4" CELLPADDING="5">
+       <TR>
+         <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#99CCFF" ALIGN="CENTER">
+           <B><FONT COLOR="#000000" SIZE="+1"><MM-List-Name> --
+       <MM-List-Description></FONT></B>
+         </TD>
+       </TR>
+       <tr>
+           <td colspan="2">
+             <p>&nbsp;
+           </td>
+         </tr>
+         <tr>
+           <TD COLSPAN="1" WIDTH="100%" BGCOLOR="#FFF0D0">
+             <B><FONT COLOR="#000000">About <MM-List-Name></FONT></B>
+           </TD>
+            <TD COLSPAN="1" WIDTH="100%" BGCOLOR="#FFF0D0">
+              <MM-lang-form-start><MM-displang-box> <MM-list-langs>
+              <MM-form-end>
+            </TD>
+         </TR>
+           <tr>
+             <td colspan="2">
+               <P><MM-List-Info></P>
+         <p> To see the collection of prior postings to the list,
+             visit the <MM-Archive><MM-List-Name>
+                 Archives</MM-Archive>.
+             <MM-Restricted-List-Message>
+         </p>
+       </TD>
+      </TR>
+      <TR>
+       <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+         <B><FONT COLOR="#000000">Using <MM-List-Name></FONT></B>
+       </TD>
+      </TR>
+      <tr>
+       <td colspan="2">
+         To post a message to all the list members, send email to
+         <A HREF="mailto:<MM-Posting-Addr>"><MM-Posting-Addr></A>.
+
+         <p>You can subscribe to the list, or change your existing
+           subscription, in the sections below.
+       </td>
+      </tr>
+      <TR>
+       <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+         <B><FONT COLOR="#000000">Subscribing to <MM-List-Name></FONT></B>
+       </TD>
+      </TR>
+      <tr>
+       <td colspan="2">
+         <P>
+           Subscribe to <MM-List-Name> by filling out the following
+             form.
+         <MM-List-Subscription-Msg>
+         <ul>
+    <MM-Subscribe-Form-Start>
+             <TABLE BORDER="0" CELLSPACING="2" CELLPADDING="2"
+               WIDTH="70%">
+               <TR>
+                 <TD BGCOLOR="#dddddd" WIDTH="55%">Your email address:</TD>
+                 <TD WIDTH="33%"><MM-Subscribe-Box>
+       </TD>
+       <TD WIDTH="12%">&nbsp;</TD></TR>
+      <tr>
+        <td bgcolor="#dddddd" width="55%">Your name (optional):</td>
+        <td width="33%"><mm-fullname-box></td>
+       <TD WIDTH="12%">&nbsp;</TD></TR>
+      <TR>
+       <TD COLSPAN="3"><FONT SIZE=-1>You may enter a
+           privacy password below. This provides only mild security,
+           but should prevent others from messing with your
+           subscription.  <b>Do not use a valuable password</b> as
+           it will occasionally be emailed back to you and <b>stored in cleartext</b>.
+
+            <br><br>If you choose not to enter a password, one will be
+            automatically generated for you, and it will be sent to
+            you once you've confirmed your subscription.  You can
+            always request a mail-back of your password when you edit
+            your personal options.
+           <MM-Reminder>
+           </font>
+       </TD>
+      </TR>
+      <TR>
+       <TD BGCOLOR="#dddddd">Pick a password:</TD>
+       <TD><MM-New-Password-Box></TD>
+       <TD>&nbsp;</TD></TR>
+      <TR>
+       <TD BGCOLOR="#dddddd">Reenter password to confirm:</TD>
+       <TD><MM-Confirm-Password></TD>
+       <TD>&nbsp; </TD></TR>
+      <tr>
+        <TD BGCOLOR="#dddddd">Which language do you prefer to display your messages?</TD>
+        <TD> <MM-list-langs></TD>
+        <TD>&nbsp; </TD></TR>
+      <mm-digest-question-start>
+      <tr>
+       <td>Would you like to receive list mail batched in a daily
+         digest?
+         </td>
+       <td><MM-Undigest-Radio-Button> No
+           <MM-Digest-Radio-Button>  Yes
+       </TD>
+      </tr>
+      <mm-digest-question-end>
+      <tr>
+        <TD BGCOLOR="#dddddd">Please answer the following question to prove that you are not a bot:
+          <mm-captcha-question>
+        </TD>
+        <TD><mm-captcha-box></TD>
+      </tr>
+      <tr>
+       <td colspan="3">
+         <center><MM-Subscribe-Button></center>
+    </td>
+    </tr>
+    </TABLE>
+    <MM-Form-End>
+    </ul>
+    <TR>
+      <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#FFF0D0">
+       <a name="subscribers">
+        <B><FONT COLOR="#000000"><MM-List-Name> Subscribers</FONT></B></a>
+      </TD>
+    </TR>
+    <tr>
+      <TD COLSPAN="2" WIDTH="100%">
+       <MM-Roster-Form-Start>
+       <MM-Roster-Option>
+           <MM-Form-End>
+         <p>
+       <MM-Options-Form-Start>
+       <MM-Editing-Options>
+               <MM-Form-End>
+      </td>
+    </tr>
+  </table>
+<MM-Mailman-Footer>
+</BODY>
+</HTML>
index 7f4aaea..b9c177e 100644 (file)
     dest: /etc/cron.daily/mailman-check
     src: files/mailman-check
     mode: u=rwx,g=rx,o=rx
+# the CAPTCHA patch (and some template modifications)
+- name: check if the files are already patched
+  shell: 'fgrep MAILMAN_CAPTCHA_PATCHED Cgi/listinfo.py && fgrep MAILMAN_CAPTCHA_PATCHED Cgi/subscribe.py'
+  args:
+    chdir: /usr/lib/mailman/Mailman
+  changed_when: False
+  register: mailman_patched
+- name: check if all the files have the right checksums to be patched
+  shell: 'echo "{{item}}" | sha256sum -c'
+  loop:
+    - "26b4cbb7c5bde8badf741e31975235e74abb932037d77d862cf00b412726c2c2  /usr/lib/mailman/Mailman/Cgi/listinfo.py"
+    - "cbef3d8cb6b65e4c9b2462f8627966d55dd52caa2e626c87241c4f8d47477dc7  /usr/lib/mailman/Mailman/Cgi/subscribe.py"
+  changed_when: False
+  when: mailman_patched.rc != 0
+- name: install patched python files
+  copy:
+    dest: /usr/lib/mailman/Mailman/{{item}}
+    src: files/mailman-patched/{{item}}
+  loop:
+  - Cgi/listinfo.py
+  - Cgi/subscribe.py
+  - Captcha.py
+- name: install patched templates
+  copy:
+    dest: /etc/mailman/{{item}}
+    src: files/mailman-patched/{{item}}
+  loop:
+  - de/listinfo.html
+  - en/listinfo.html
index 9464272..89dd0d8 100644 (file)
@@ -1,4 +1,5 @@
 # -*- python -*-
+# -*- coding: utf-8 -*-
 
 # Copyright (C) 1998,1999,2000 by the Free Software Foundation, Inc.
 #
@@ -131,3 +132,12 @@ DEFAULT_DMARC_MODERATION_ACTION = 1 # Munge From
 
 # Spammer protection
 SUBSCRIBE_FORM_SECRET = "{{postfix.mailman.form_secret}}"
+CAPTCHAS = [
+{% for item in postfix.mailman.captcha %}
+    ('{{item.question}}', [
+    {% for answer in item.answers %}
+        '{{answer}}',
+    {% endfor %}
+    ]),
+{% endfor %}
+]