1 # Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
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.
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.
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,
18 """Process subscription or roster requests from listinfo form."""
29 from Mailman import mm_cfg
30 from Mailman import Utils
31 from Mailman import Captcha # MAILMAN_CAPTCHA_PATCHED
32 from Mailman import MailList
33 from Mailman import Errors
34 from Mailman import i18n
35 from Mailman import Message
36 from Mailman.UserDesc import UserDesc
37 from Mailman.htmlformat import *
38 from Mailman.Logging.Syslog import syslog
46 i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
52 doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
54 parts = Utils.GetPathPieces()
56 doc.AddItem(Header(2, _("Error")))
57 doc.AddItem(Bold(_('Invalid options to CGI script')))
61 listname = parts[0].lower()
63 mlist = MailList.MailList(listname, lock=0)
64 except Errors.MMListError, e:
65 # Avoid cross-site scripting attacks
66 safelistname = Utils.websafe(listname)
67 doc.AddItem(Header(2, _("Error")))
68 doc.AddItem(Bold(_('No such list <em>%(safelistname)s</em>')))
69 # Send this with a 404 status.
70 print 'Status: 404 Not Found'
72 syslog('error', 'subscribe: No such list "%s": %s\n', listname, e)
75 # See if the form data has a preferred language set, in which case, use it
76 # for the results. If not, use the list's preferred language.
77 cgidata = cgi.FieldStorage()
79 language = cgidata.getfirst('language', '')
81 # Someone crafted a POST with a bad Content-Type:.
82 doc.AddItem(Header(2, _("Error")))
83 doc.AddItem(Bold(_('Invalid options to CGI script.')))
84 # Send this with a 400 status.
85 print 'Status: 400 Bad Request'
88 if not Utils.IsLanguage(language):
89 language = mlist.preferred_language
90 i18n.set_language(language)
91 doc.set_language(language)
93 # We need a signal handler to catch the SIGTERM that can come from Apache
94 # when the user hits the browser's STOP button. See the comment in
95 # admin.py for details.
97 # BAW: Strictly speaking, the list should not need to be locked just to
98 # read the request database. However the request database asserts that
99 # the list is locked in order to load it and it's not worth complicating
101 def sigterm_handler(signum, frame, mlist=mlist):
102 # Make sure the list gets unlocked...
104 # ...and ensure we exit, otherwise race conditions could cause us to
105 # enter MailList.Save() while we're in the unlocked state, and that
111 # Install the emergency shutdown signal handler
112 signal.signal(signal.SIGTERM, sigterm_handler)
114 process_form(mlist, doc, cgidata, language)
121 def process_form(mlist, doc, cgidata, lang):
122 listowner = mlist.GetOwnerEmail()
123 realname = mlist.real_name
126 # The email address being subscribed, required
127 email = cgidata.getfirst('email', '').strip()
129 results.append(_('You must supply a valid email address.'))
131 fullname = cgidata.getfirst('fullname', '')
132 # Canonicalize the full name
133 fullname = Utils.canonstr(fullname, lang)
134 # Who was doing the subscribing?
135 remote = os.environ.get('HTTP_FORWARDED_FOR',
136 os.environ.get('HTTP_X_FORWARDED_FOR',
137 os.environ.get('REMOTE_ADDR',
138 'unidentified origin')))
140 # Check reCAPTCHA submission, if enabled
141 if mm_cfg.RECAPTCHA_SECRET_KEY:
142 request = urllib2.Request(
143 url = 'https://www.google.com/recaptcha/api/siteverify',
144 data = urllib.urlencode({
145 'secret': mm_cfg.RECAPTCHA_SECRET_KEY,
146 'response': cgidata.getvalue('g-recaptcha-response', ''),
147 'remoteip': remote}))
149 httpresp = urllib2.urlopen(request)
150 captcha_response = json.load(httpresp)
152 if not captcha_response['success']:
153 e_codes = COMMASPACE.join(captcha_response['error-codes'])
154 results.append(_('reCAPTCHA validation failed: %(e_codes)s'))
155 except urllib2.URLError, e:
157 results.append(_('reCAPTCHA could not be validated: %(e_reason)s'))
159 # Are we checking the hidden data?
160 if mm_cfg.SUBSCRIBE_FORM_SECRET:
161 now = int(time.time())
162 # Try to accept a range in case of load balancers, etc. (LP: #1447445)
163 if remote.find('.') >= 0:
164 # ipv4 - drop last octet
165 remote1 = remote.rsplit('.', 1)[0]
167 # ipv6 - drop last 16 (could end with :: in which case we just
168 # drop one : resulting in an invalid format, but it's only
169 # for our hash so it doesn't matter.
170 remote1 = remote.rsplit(':', 1)[0]
172 ftime, fcaptcha_idx, fhash = cgidata.getfirst('sub_form_token', '').split(':')
175 ftime = fcaptcha_idx = fhash = ''
177 token = Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + ":" +
180 mlist.internal_name() + ":" +
182 if ftime and now - then > mm_cfg.FORM_LIFETIME:
183 results.append(_('The form is too old. Please GET it again.'))
184 if ftime and now - then < mm_cfg.SUBSCRIBE_FORM_MIN_TIME:
186 _('Please take a few seconds to fill out the form before submitting it.'))
187 if ftime and token != fhash:
189 _("The hidden token didn't match. Did your IP change?"))
192 _('There was no hidden token in your submission or it was corrupted.'))
193 results.append(_('You must GET the form before submitting it.'))
195 captcha_answer = cgidata.getvalue('captcha_answer', '')
196 if not Captcha.verify(fcaptcha_idx, captcha_answer, mm_cfg.CAPTCHAS):
197 results.append(_('This was not the right answer to the CAPTCHA question.'))
198 # Was an attempt made to subscribe the list to itself?
199 if email == mlist.GetListEmail():
200 syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote)
201 results.append(_('You may not subscribe a list to itself!'))
202 # If the user did not supply a password, generate one for him
203 password = cgidata.getfirst('pw', '').strip()
204 confirmed = cgidata.getfirst('pw-conf', '').strip()
206 if not password and not confirmed:
207 password = Utils.MakeRandomPassword()
208 elif not password or not confirmed:
209 results.append(_('If you supply a password, you must confirm it.'))
210 elif password <> confirmed:
211 results.append(_('Your passwords did not match.'))
213 # Get the digest option for the subscription.
214 digestflag = cgidata.getfirst('digest')
217 digest = int(digestflag)
218 except (TypeError, ValueError):
221 digest = mlist.digest_is_default
223 # Sanity check based on list configuration. BAW: It's actually bogus that
224 # the page allows you to set the digest flag if you don't really get the
226 if not mlist.digestable:
228 elif not mlist.nondigestable:
232 print_results(mlist, ERRORSEP.join(results), doc, lang)
235 # If this list has private rosters, we have to be careful about the
236 # message that gets printed, otherwise the subscription process can be
237 # used to mine for list members. It may be inefficient, but it's still
238 # possible, and that kind of defeats the purpose of private rosters.
239 # We'll use this string for all successful or unsuccessful subscription
241 if mlist.private_roster == 0:
245 privacy_results = _("""\
246 Your subscription request has been received, and will soon be acted upon.
247 Depending on the configuration of this mailing list, your subscription request
248 may have to be first confirmed by you via email, or approved by the list
249 moderator. If confirmation is required, you will soon get a confirmation
250 email which contains further instructions.""")
253 userdesc = UserDesc(email, fullname, password, digest, lang)
254 mlist.AddMember(userdesc, remote)
256 # Check for all the errors that mlist.AddMember can throw options on the
257 # web page for this cgi
258 except Errors.MembershipIsBanned:
259 results = _("""The email address you supplied is banned from this
260 mailing list. If you think this restriction is erroneous, please
261 contact the list owners at %(listowner)s.""")
262 except Errors.MMBadEmailError:
264 The email address you supplied is not valid. (E.g. it must contain an
266 except Errors.MMHostileAddress:
268 Your subscription is not allowed because the email address you gave is
270 except Errors.MMSubscribeNeedsConfirmation:
271 # Results string depends on whether we have private rosters or not
273 results = privacy_results
276 Confirmation from your email address is required, to prevent anyone from
277 subscribing you without permission. Instructions are being sent to you at
278 %(email)s. Please note your subscription will not start until you confirm
279 your subscription.""")
280 except Errors.MMNeedApproval, x:
281 # Results string depends on whether we have private rosters or not
283 results = privacy_results
285 # We need to interpolate into x.__str__()
288 Your subscription request was deferred because %(x)s. Your request has been
289 forwarded to the list moderator. You will receive email informing you of the
290 moderator's decision when they get to your request.""")
291 except Errors.MMAlreadyAMember:
292 # Results string depends on whether we have private rosters or not
293 if not privacy_results:
294 results = _('You are already subscribed.')
296 results = privacy_results
297 # This could be a membership probe. For safety, let the user know
298 # a probe occurred. BAW: should we inform the list moderator?
299 listaddr = mlist.GetListEmail()
300 # Set the language for this email message to the member's language.
301 mlang = mlist.getMemberLanguage(email)
302 otrans = i18n.get_translation()
303 i18n.set_language(mlang)
305 msg = Message.UserNotification(
306 mlist.getMemberCPAddress(email),
307 mlist.GetBouncesEmail(),
308 _('Mailman privacy alert'),
310 An attempt was made to subscribe your address to the mailing list
311 %(listaddr)s. You are already subscribed to this mailing list.
313 Note that the list membership is not public, so it is possible that a bad
314 person was trying to probe the list for its membership. This would be a
315 privacy violation if we let them do this, but we didn't.
317 If you submitted the subscription request and forgot that you were already
318 subscribed to the list, then you can ignore this message. If you suspect that
319 an attempt is being made to covertly discover whether you are a member of this
320 list, and you are worried about your privacy, then feel free to send a message
321 to the list administrator at %(listowner)s.
324 i18n.set_translation(otrans)
326 # These shouldn't happen unless someone's tampering with the form
327 except Errors.MMCantDigestError:
328 results = _('This list does not support digest delivery.')
329 except Errors.MMMustDigestError:
330 results = _('This list only supports digest delivery.')
332 # Everything's cool. Our return string actually depends on whether
333 # this list has private rosters or not
335 results = privacy_results
338 You have been successfully subscribed to the %(realname)s mailing list.""")
340 print_results(mlist, results, doc, lang)
344 def print_results(mlist, results, doc, lang):
345 # The bulk of the document will come from the options.html template, which
346 # includes its own html armor (head tags, etc.). Suppress the head that
347 # Document() derived pages get automatically.
348 doc.suppress_head = 1
350 replacements = mlist.GetStandardReplacements(lang)
351 replacements['<mm-results>'] = results
352 output = mlist.ParseTags('subscribe.html', replacements, lang)