1 # Copyright (C) 1998-2016 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."""
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
42 i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
49 doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
51 parts = Utils.GetPathPieces()
53 doc.AddItem(Header(2, _("Error")))
54 doc.AddItem(Bold(_('Invalid options to CGI script')))
58 listname = parts[0].lower()
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'
69 syslog('error', 'subscribe: No such list "%s": %s\n', listname, e)
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()
76 language = cgidata.getvalue('language', '')
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'
85 if not Utils.IsLanguage(language):
86 language = mlist.preferred_language
87 i18n.set_language(language)
88 doc.set_language(language)
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.
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
98 def sigterm_handler(signum, frame, mlist=mlist):
99 # Make sure the list gets unlocked...
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
108 # Install the emergency shutdown signal handler
109 signal.signal(signal.SIGTERM, sigterm_handler)
111 process_form(mlist, doc, cgidata, language)
119 def process_form(mlist, doc, cgidata, lang):
120 listowner = mlist.GetOwnerEmail()
121 realname = mlist.real_name
124 # The email address being subscribed, required
125 email = cgidata.getvalue('email', '').strip()
127 results.append(_('You must supply a valid email address.'))
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]
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]
150 ftime, fcaptcha_idx, fhash = cgidata.getvalue('sub_form_token', '').split(':')
153 ftime = fcaptcha_idx = fhash = ''
155 token = Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET +
158 mlist.internal_name() +
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:
164 _('Please take a few seconds to fill out the form before submitting it.'))
165 if ftime and token != fhash:
167 _("The hidden token didn't match. Did your IP change?"))
170 _('There was no hidden token in your submission or it was corrupted.'))
171 results.append(_('You must GET the form before submitting it.'))
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()
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.'))
191 # Get the digest option for the subscription.
192 digestflag = cgidata.getvalue('digest')
195 digest = int(digestflag)
199 digest = mlist.digest_is_default
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
204 if not mlist.digestable:
206 elif not mlist.nondigestable:
210 print_results(mlist, ERRORSEP.join(results), doc, lang)
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
219 if mlist.private_roster == 0:
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.""")
231 userdesc = UserDesc(email, fullname, password, digest, lang)
232 mlist.AddMember(userdesc, remote)
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:
242 The email address you supplied is not valid. (E.g. it must contain an
244 except Errors.MMHostileAddress:
246 Your subscription is not allowed because the email address you gave is
248 except Errors.MMSubscribeNeedsConfirmation:
249 # Results string depends on whether we have private rosters or not
251 results = privacy_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
261 results = privacy_results
263 # We need to interpolate into x.__str__()
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.')
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)
283 msg = Message.UserNotification(
284 mlist.getMemberCPAddress(email),
285 mlist.GetBouncesEmail(),
286 _('Mailman privacy alert'),
288 An attempt was made to subscribe your address to the mailing list
289 %(listaddr)s. You are already subscribed to this mailing list.
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.
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.
302 i18n.set_translation(otrans)
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.')
310 # Everything's cool. Our return string actually depends on whether
311 # this list has private rosters or not
313 results = privacy_results
316 You have been successfully subscribed to the %(realname)s mailing list.""")
318 print_results(mlist, results, doc, lang)
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
329 replacements = mlist.GetStandardReplacements(lang)
330 replacements['<mm-results>'] = results
331 output = mlist.ParseTags('subscribe.html', replacements, lang)