patch mailman to add a simple question-and-answer CAPTCHA
[ansible.git] / roles / email / files / mailman-patched / Cgi / subscribe.py
1 # Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
17
18 """Process subscription or roster requests from listinfo form."""
19
20 import sys
21 import os
22 import cgi
23 import time
24 import signal
25
26 from Mailman import mm_cfg
27 from Mailman import Utils
28 from Mailman import Captcha # MAILMAN_CAPTCHA_PATCHED
29 from Mailman import MailList
30 from Mailman import Errors
31 from Mailman import i18n
32 from Mailman import Message
33 from Mailman.UserDesc import UserDesc
34 from Mailman.htmlformat import *
35 from Mailman.Logging.Syslog import syslog
36
37 SLASH = '/'
38 ERRORSEP = '\n\n<p>'
39
40 # Set up i18n
41 _ = i18n._
42 i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
43
44
45
46
47 def main():
48     doc = Document()
49     doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
50
51     parts = Utils.GetPathPieces()
52     if not parts:
53         doc.AddItem(Header(2, _("Error")))
54         doc.AddItem(Bold(_('Invalid options to CGI script')))
55         print doc.Format()
56         return
57
58     listname = parts[0].lower()
59     try:
60         mlist = MailList.MailList(listname, lock=0)
61     except Errors.MMListError, e:
62         # Avoid cross-site scripting attacks
63         safelistname = Utils.websafe(listname)
64         doc.AddItem(Header(2, _("Error")))
65         doc.AddItem(Bold(_('No such list <em>%(safelistname)s</em>')))
66         # Send this with a 404 status.
67         print 'Status: 404 Not Found'
68         print doc.Format()
69         syslog('error', 'subscribe: No such list "%s": %s\n', listname, e)
70         return
71
72     # See if the form data has a preferred language set, in which case, use it
73     # for the results.  If not, use the list's preferred language.
74     cgidata = cgi.FieldStorage()
75     try:
76         language = cgidata.getvalue('language', '')
77     except TypeError:
78         # Someone crafted a POST with a bad Content-Type:.
79         doc.AddItem(Header(2, _("Error")))
80         doc.AddItem(Bold(_('Invalid options to CGI script.')))
81         # Send this with a 400 status.
82         print 'Status: 400 Bad Request'
83         print doc.Format()
84         return
85     if not Utils.IsLanguage(language):
86         language = mlist.preferred_language
87     i18n.set_language(language)
88     doc.set_language(language)
89
90     # We need a signal handler to catch the SIGTERM that can come from Apache
91     # when the user hits the browser's STOP button.  See the comment in
92     # admin.py for details.
93     #
94     # BAW: Strictly speaking, the list should not need to be locked just to
95     # read the request database.  However the request database asserts that
96     # the list is locked in order to load it and it's not worth complicating
97     # that logic.
98     def sigterm_handler(signum, frame, mlist=mlist):
99         # Make sure the list gets unlocked...
100         mlist.Unlock()
101         # ...and ensure we exit, otherwise race conditions could cause us to
102         # enter MailList.Save() while we're in the unlocked state, and that
103         # could be bad!
104         sys.exit(0)
105
106     mlist.Lock()
107     try:
108         # Install the emergency shutdown signal handler
109         signal.signal(signal.SIGTERM, sigterm_handler)
110
111         process_form(mlist, doc, cgidata, language)
112         mlist.Save()
113     finally:
114         mlist.Unlock()
115
116
117
118
119 def process_form(mlist, doc, cgidata, lang):
120     listowner = mlist.GetOwnerEmail()
121     realname = mlist.real_name
122     results = []
123
124     # The email address being subscribed, required
125     email = cgidata.getvalue('email', '').strip()
126     if not email:
127         results.append(_('You must supply a valid email address.'))
128
129     fullname = cgidata.getvalue('fullname', '')
130     # Canonicalize the full name
131     fullname = Utils.canonstr(fullname, lang)
132     # Who was doing the subscribing?
133     remote = os.environ.get('HTTP_FORWARDED_FOR',
134              os.environ.get('HTTP_X_FORWARDED_FOR',
135              os.environ.get('REMOTE_ADDR',
136                             'unidentified origin')))
137     # Are we checking the hidden data?
138     if mm_cfg.SUBSCRIBE_FORM_SECRET:
139         now = int(time.time())
140         # Try to accept a range in case of load balancers, etc.  (LP: #1447445)
141         if remote.find('.') >= 0:
142             # ipv4 - drop last octet
143             remote1 = remote.rsplit('.', 1)[0]
144         else:
145             # ipv6 - drop last 16 (could end with :: in which case we just
146             #        drop one : resulting in an invalid format, but it's only
147             #        for our hash so it doesn't matter.
148             remote1 = remote.rsplit(':', 1)[0]
149         try:
150             ftime, fcaptcha_idx, fhash = cgidata.getvalue('sub_form_token', '').split(':')
151             then = int(ftime)
152         except ValueError:
153             ftime = fcaptcha_idx = fhash = ''
154             then = 0
155         token = Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET +
156                               ftime +
157                               fcaptcha_idx +
158                               mlist.internal_name() +
159                               remote1).hexdigest()
160         if ftime and now - then > mm_cfg.FORM_LIFETIME:
161             results.append(_('The form is too old.  Please GET it again.'))
162         if ftime and now - then < mm_cfg.SUBSCRIBE_FORM_MIN_TIME:
163             results.append(
164     _('Please take a few seconds to fill out the form before submitting it.'))
165         if ftime and token != fhash:
166             results.append(
167                 _("The hidden token didn't match.  Did your IP change?"))
168         if not ftime:
169             results.append(
170     _('There was no hidden token in your submission or it was corrupted.'))
171             results.append(_('You must GET the form before submitting it.'))
172         # Check captcha
173         captcha_answer = cgidata.getvalue('captcha_answer', '')
174         if not Captcha.verify(fcaptcha_idx, captcha_answer, mm_cfg.CAPTCHAS):
175             results.append(_('This was not the right answer to the CAPTCHA question.'))
176     # Was an attempt made to subscribe the list to itself?
177     if email == mlist.GetListEmail():
178         syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote)
179         results.append(_('You may not subscribe a list to itself!'))
180     # If the user did not supply a password, generate one for him
181     password = cgidata.getvalue('pw', '').strip()
182     confirmed = cgidata.getvalue('pw-conf', '').strip()
183
184     if not password and not confirmed:
185         password = Utils.MakeRandomPassword()
186     elif not password or not confirmed:
187         results.append(_('If you supply a password, you must confirm it.'))
188     elif password <> confirmed:
189         results.append(_('Your passwords did not match.'))
190
191     # Get the digest option for the subscription.
192     digestflag = cgidata.getvalue('digest')
193     if digestflag:
194         try:
195             digest = int(digestflag)
196         except ValueError:
197             digest = 0
198     else:
199         digest = mlist.digest_is_default
200
201     # Sanity check based on list configuration.  BAW: It's actually bogus that
202     # the page allows you to set the digest flag if you don't really get the
203     # choice. :/
204     if not mlist.digestable:
205         digest = 0
206     elif not mlist.nondigestable:
207         digest = 1
208
209     if results:
210         print_results(mlist, ERRORSEP.join(results), doc, lang)
211         return
212
213     # If this list has private rosters, we have to be careful about the
214     # message that gets printed, otherwise the subscription process can be
215     # used to mine for list members.  It may be inefficient, but it's still
216     # possible, and that kind of defeats the purpose of private rosters.
217     # We'll use this string for all successful or unsuccessful subscription
218     # results.
219     if mlist.private_roster == 0:
220         # Public rosters
221         privacy_results = ''
222     else:
223         privacy_results = _("""\
224 Your subscription request has been received, and will soon be acted upon.
225 Depending on the configuration of this mailing list, your subscription request
226 may have to be first confirmed by you via email, or approved by the list
227 moderator.  If confirmation is required, you will soon get a confirmation
228 email which contains further instructions.""")
229
230     try:
231         userdesc = UserDesc(email, fullname, password, digest, lang)
232         mlist.AddMember(userdesc, remote)
233         results = ''
234     # Check for all the errors that mlist.AddMember can throw options on the
235     # web page for this cgi
236     except Errors.MembershipIsBanned:
237         results = _("""The email address you supplied is banned from this
238         mailing list.  If you think this restriction is erroneous, please
239         contact the list owners at %(listowner)s.""")
240     except Errors.MMBadEmailError:
241         results = _("""\
242 The email address you supplied is not valid.  (E.g. it must contain an
243 `@'.)""")
244     except Errors.MMHostileAddress:
245         results = _("""\
246 Your subscription is not allowed because the email address you gave is
247 insecure.""")
248     except Errors.MMSubscribeNeedsConfirmation:
249         # Results string depends on whether we have private rosters or not
250         if privacy_results:
251             results = privacy_results
252         else:
253             results = _("""\
254 Confirmation from your email address is required, to prevent anyone from
255 subscribing you without permission.  Instructions are being sent to you at
256 %(email)s.  Please note your subscription will not start until you confirm
257 your subscription.""")
258     except Errors.MMNeedApproval, x:
259         # Results string depends on whether we have private rosters or not
260         if privacy_results:
261             results = privacy_results
262         else:
263             # We need to interpolate into x.__str__()
264             x = _(str(x))
265             results = _("""\
266 Your subscription request was deferred because %(x)s.  Your request has been
267 forwarded to the list moderator.  You will receive email informing you of the
268 moderator's decision when they get to your request.""")
269     except Errors.MMAlreadyAMember:
270         # Results string depends on whether we have private rosters or not
271         if not privacy_results:
272             results = _('You are already subscribed.')
273         else:
274             results = privacy_results
275             # This could be a membership probe.  For safety, let the user know
276             # a probe occurred.  BAW: should we inform the list moderator?
277             listaddr = mlist.GetListEmail()
278             # Set the language for this email message to the member's language.
279             mlang = mlist.getMemberLanguage(email)
280             otrans = i18n.get_translation()
281             i18n.set_language(mlang)
282             try:
283                 msg = Message.UserNotification(
284                     mlist.getMemberCPAddress(email),
285                     mlist.GetBouncesEmail(),
286                     _('Mailman privacy alert'),
287                     _("""\
288 An attempt was made to subscribe your address to the mailing list
289 %(listaddr)s.  You are already subscribed to this mailing list.
290
291 Note that the list membership is not public, so it is possible that a bad
292 person was trying to probe the list for its membership.  This would be a
293 privacy violation if we let them do this, but we didn't.
294
295 If you submitted the subscription request and forgot that you were already
296 subscribed to the list, then you can ignore this message.  If you suspect that
297 an attempt is being made to covertly discover whether you are a member of this
298 list, and you are worried about your privacy, then feel free to send a message
299 to the list administrator at %(listowner)s.
300 """), lang=mlang)
301             finally:
302                 i18n.set_translation(otrans)
303             msg.send(mlist)
304     # These shouldn't happen unless someone's tampering with the form
305     except Errors.MMCantDigestError:
306         results = _('This list does not support digest delivery.')
307     except Errors.MMMustDigestError:
308         results = _('This list only supports digest delivery.')
309     else:
310         # Everything's cool.  Our return string actually depends on whether
311         # this list has private rosters or not
312         if privacy_results:
313             results = privacy_results
314         else:
315             results = _("""\
316 You have been successfully subscribed to the %(realname)s mailing list.""")
317     # Show the results
318     print_results(mlist, results, doc, lang)
319
320
321
322
323 def print_results(mlist, results, doc, lang):
324     # The bulk of the document will come from the options.html template, which
325     # includes its own html armor (head tags, etc.).  Suppress the head that
326     # Document() derived pages get automatically.
327     doc.suppress_head = 1
328
329     replacements = mlist.GetStandardReplacements(lang)
330     replacements['<mm-results>'] = results
331     output = mlist.ParseTags('subscribe.html', replacements, lang)
332     doc.AddItem(output)
333     print doc.Format()