here is a patch that implements email confirmations. as was discussed
previously on this list, all subscriptions go through the
confirmation process. in addition, the subscribe mail command has
changed to allow subsciptions like this:
subscribe [password] [digest|nodigest] [address=<some address>]
this will allow people to subscribe addresses other than the one they
are sending from.
Finally, regarding whether or not someone can be subscribed by just
replying to the confirmation message, they can. they can also mail
the command
confirm <confirmation number>
this patch is against mailman 1.0b3. it does not address the issues
regarding a -confirm address and bounced confirmation requests that
were discussed today on this list.
this patch consists of 2 parts: first, the patch, and second, a file
called mm_pending.py to go in the mailman/modules directory.
scott
(coming soon: admin cgi to use cookie confirmation and
way enhanced membership management page)
--------------------------------file mm_pending.py----------------
"""
module for handling pending subscriptions
"""
import os
import sys
import posixfile
import marshal
import time
import whrandom
import mm_cfg
DB_PATH = mm_cfg.MAILMAN_DIR + "/misc/pending_subscriptions.db"
LOCK_PATH = mm_cfg.LOCK_DIR + "/pending_subscriptions.lock"
VERIFY_FMT = """\
%(listname)s -- confirmation of subscription -- request %(cookie)s
You or someone (%(requestor)s) has requested that your email
address (%(email)s) be subscribed to the %(listname)s mailling
list at %(listaddress)s. If you wish to fulfill this request,
please simply reply to this message, or mail %(request_addr)s
with the following line, and only the following line in the
message body:
confirm %(cookie)s
If you do not wish to subscribe to this list, please simply ignore
or delete this message.
"""
# ' icky emacs font lock thing
def get_pending():
" returns a dict containing pending information"
try:
fp = open(DB_PATH,"r" )
except IOError:
return {}
dict = marshal.load(fp)
return dict
def gencookie(p=None):
if p is None:
p = get_pending()
while 1:
newcookie = int(whrandom.random() * 1000000)
if p.has_key(newcookie) or newcookie < 100000:
continue
return newcookie
def set_pending(p):
ou = os.umask(0)
try:
lock_file = posixfile.open(LOCK_PATH,'a+')
finally:
os.umask(ou)
lock_file.lock('w|', 1)
fp = open(DB_PATH, "w")
marshal.dump(p, fp)
fp.close()
lock_file.lock("u")
lock_file.close()
def add2pending(email_addr, password, digest, cookie):
ts = int(time.time())
processed = 0
p = get_pending()
p[cookie] = (email_addr, password, digest, ts)
set_pending(p)
----------------------------patches-----------------------------
Index: cgi/subscribe
===================================================================
RCS file: /usr/local/cvsroot/mailman/cgi/subscribe,v
retrieving revision 1.2
diff -r1.2 subscribe
12c12
< import mm_utils, maillist, mm_err, mm_message, mm_cfg, htmlformat
---
> import mm_utils, maillist, mm_err, mm_message, mm_cfg, mm_pending, htmlformat
20,38d19
< NEED_CONFIRM_NOTICE = """
< A request for subscription of your address to the %s@%s
< mailing list has been received via the web%s
<
< This is a confirmation request, to prevent anyone from subscribing
< you against your wishes. In order to complete this subscription you
< must send a confirming email, to %s, by
< replying to this mail, and including just the line:
<
< subscribe %s%s
<
< in the body or as the subject line. If you do not actually wish to
< subscribe you need not do anything. Upon subscribing you will receive
< a message welcoming you to the list and describing how to tailor your
< account.
<
< Questions or comments?
< Send them to """ + mm_cfg.MAILMAN_OWNER
<
146d126
<
148,165c128,137
< list.AddMember(email, pw, digest, web_subscribe=1)
< results = results + ("You have successfully been added. "
< "You should receive confirmation by "
< "e-mail within an hour. If you do not "
< "receive confirmation, then the email "
< "address you gave probably bounced, "
< "in which case you should try again.<p>")
< except mm_err.MMWebSubscribeRequiresConfirmation:
< results = results + ("Confirmation from your email address is "
< "required, to prevent anyone from covertly "
< "subscribing you. Instructions are being "
< "sent to you at %s." % email)
< if os.environ.has_key('REMOTE_HOST'):
< remote = ", from\n%s. " % os.environ['REMOTE_HOST']
< elif os.environ.has_key('REMOTE_ADDR'):
< remote = ", from\n%s." % os.environ['REMOTE_ADDR']
< else:
< remote = "."
---
> results = results + ("Confirmation from your email address is "
> "required, to prevent anyone from covertly "
> "subscribing you. Instructions are being "
> "sent to you at %s." % email)
> if os.environ.has_key('REMOTE_HOST'):
> remote = os.environ['REMOTE_HOST']
> elif os.environ.has_key('REMOTE_ADDR'):
> remote = os.environ['REMOTE_ADDR']
> else:
> remote = "."
170,179c142,154
< list.SendTextToUser(subject = 'Subscribing to %s' % list.real_name,
< recipient = email,
< sender = list.GetAdminEmail(),
< text = (NEED_CONFIRM_NOTICE
< % (list.real_name,
< list.host_name,
< remote,
< list.GetRequestEmail(),
< pw,
< digesting)),
---
> cookie = mm_pending.gencookie()
> mm_pending.add2pending(email, pw, digest, cookie)
> list.SendTextToUser(subject = "%s -- confirmation of subscription -- request %d" % \
> (list.real_name, cookie),
> recipient = email,
> sender = list.GetRequestEmail(),
> text = mm_pending.VERIFY_FMT % ({"email": email,
> "listaddress": list.GetListEmail(),
> "listname": list.real_name,
> "cookie": cookie,
> "requestor": remote,
> "request_addr": list.GetRequestEmail()}),
>
183c158
< % list.GetAdminEmail()])
---
> % list.GetAdminEmail()])
191,197c166,176
< except mm_err.MMNeedApproval, x:
< results = results + ("Subscription was <em>deferred</em> "
< "because:<br> %s<p>Your request must "
< "be approved by the list admin. "
< "You will receive email informing you "
< "of the moderator's descision when they "
< "get to your request.<p>" % x)
---
> #
> # deprecating this, it might be useful if we decide to
> # allow approved based subscriptions without confirmation
> #
> ## except mm_err.MMNeedApproval, x:
> ## results = results + ("Subscription was <em>deferred</em> "
> ## "because:<br> %s<p>Your request must "
> ## "be approved by the list admin. "
> ## "You will receive email informing you "
> ## "of the moderator's descision when they "
> ## "get to your request.<p>" % x)
206a186,192
>
>
>
>
>
>
>
Index: modules/maillist.py
===================================================================
RCS file: /usr/local/cvsroot/mailman/modules/maillist.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -r1.2 -r1.3
142,143d141
< self.web_subscribe_requires_confirmation = \
< mm_cfg.DEFAULT_WEB_SUBSCRIBE_REQUIRES_CONFIRMATION
312,322d309
<
< ('web_subscribe_requires_confirmation', mm_cfg.Radio,
< ('None', 'Requestor confirms via email', 'Admin approves'), 0,
< 'What confirmation is required for on-the-web subscribes?',
<
< "This option determines whether web-initiated subscribes"
< " require further confirmation, either from the subscribed"
< " address or from the list administrator. Absence of"
< " <em>any</em> confirmation makes web-based subscription a"
< " tempting opportunity for mischievous subscriptions by third"
< " parties."),
Index: modules/mm_defaults.py
===================================================================
RCS file: /usr/local/cvsroot/mailman/modules/mm_defaults.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -r1.2 -r1.3
93,94d92
< # 1 for email subscription verification, 2 for admin confirmation:
< DEFAULT_WEB_SUBSCRIBE_REQUIRES_CONFIRMATION = 1
Index: modules/mm_mailcmd.py
===================================================================
RCS file: /usr/local/cvsroot/mailman/modules/mm_mailcmd.py,v
retrieving revision 1.2
diff -r1.2 mm_mailcmd.py
8,9c8,9
< import string, os, sys
< import mm_message, mm_err, mm_cfg, mm_utils
---
> import string, os, sys, re
> import mm_message, mm_err, mm_cfg, mm_utils, mm_pending
41a42
> 'confirm': self.ProcessConfirmCmd,
50a52
> self.__NoMailCmdResponse = 0
84a87,94
> #
> # check to see if confirmation request -- special handling
> #
> conf_pat = r"%s -- confirmation of subscription -- request (\d\d\d\d\d\d)" % \
> self.real_name
> match = re.search(conf_pat, mail.body)
> if match:
> lines = ["confirm %s" % (match.group(1))]
103c113,114
< self.SendMailCmdResponse(mail)
---
> if not self.__NoMailCmdResponse:
> self.SendMailCmdResponse(mail)
358a370,372
> password = ""
> address = ""
> done_digest = 0
362,369c376,396
< elif len(args) == 1:
< if string.lower(args[0]) == 'digest':
< digest = 1
< password = "%s%s" % (mm_utils.GetRandomSeed(),
< mm_utils.GetRandomSeed())
< elif string.lower(args[0]) == 'nodigest':
< digest = 0
< password = "%s%s" % (mm_utils.GetRandomSeed(),
---
> elif len(args) > 3:
> self.AddError("Usage: subscribe [password] [digest|nodigest] [address=<email-address>]")
> return
> else:
> for arg in args:
> if string.lower(arg) == 'digest' and not done_digest:
> digest = 1
> done_digest = 1
> elif string.lower(arg) == 'nodigest' and not done_digest:
> digest = 0
> done_digest = 1
> elif string.lower(arg)[:8] == 'address=' and not address:
> address = string.lower(arg)[8:]
> elif not password:
> password = arg
> else:
> self.AddError("Usage: subscribe [password] "
> "[digest|nodigest] [address=<email-address>]")
> return
> if not password:
> password = "%s%s" % (mm_utils.GetRandomSeed(),
371,372c398,415
< else:
< password = args[0]
---
> if not address:
> pending_addr = mail.GetSender()
> else:
> pending_addr = address
> cookie = mm_pending.gencookie()
> mm_pending.add2pending(pending_addr, password, digest, cookie)
> self.SendTextToUser(subject = "%s -- confirmation of subscription -- request %d" % \
> (self.real_name, cookie),
> recipient = pending_addr,
> sender = self.GetRequestEmail(),
> text = mm_pending.VERIFY_FMT % ({"email": pending_addr,
> "listaddress": self.GetListEmail(),
> "listname": self.real_name,
> "cookie": cookie,
> "requestor": mail.GetSender(),
> "request_addr": self.GetRequestEmail()}))
> self.__NoMailCmdResponse = 1
> return
374,392d416
< elif len(args) == 2:
< if string.lower(args[1]) == 'nodigest':
< digest = 0
< password = args[0]
< elif string.lower(args[1]) == 'digest':
< digest = 1
< password = args[0]
< elif string.lower(args[0]) == 'nodigest':
< digest = 0
< password = args[1]
< elif string.lower(args[0]) == 'digest':
< digest = 1
< password = args[1]
< else:
< self.AddError("Usage: subscribe [password] [digest|nodigest]")
< return
< elif len(args) > 2:
< self.AddError("Usage: subscribe [password] [digest|nodigest]")
< return
393a418
> def FinishSubscribe(self, addr, password, digest):
395c420
< self.AddMember(mail.GetSender(), password, digest)
---
> self.AddMember(addr, password, digest)
399c424
< mail.GetSender())
---
> addr)
410c435
< "(insecure address)" % mail.GetSender())
---
> "(insecure address)" % addr)
412c437
< self.AddError("%s is already a list member." % mail.GetSender())
---
> self.AddError("%s is already a list member." % addr)
418a444,470
>
>
>
> def ProcessConfirmCmd(self, args, cmd, mail):
> if len(args) != 1:
> self.AddError("Usage: confirm <confirmation number>\n")
> return
> try:
> cookie = string.atoi(args[0])
> except:
> self.AddError("Usage: confirm <confirmation number>\n")
> return
> pending = mm_pending.get_pending()
> if not pending.has_key(cookie):
> self.AddError("Invalid confirmation number!\n"
> "Please recheck the confirmation number and try again.")
> return
> (email_addr, password, digest, ts) = pending[cookie]
> if self.open_subscribe:
> self.ApprovedAddMember(email_addr, password, digest)
> self.AddToResponse("Succeeded")
> else:
> self.AddRequest('add_member', digest, email_addr, password)
> del pending[cookie]
> mm_pending.set_pending(pending)
>
>
464c516
< subscribe [password] [digest-option]
---
> subscribe [password] [digest-option] [address=<address>]
469,470c521,523
< To subscribe this way, you must subscribe from the account in
< which you wish to receive mail.
---
> If you wish to subscribe an address other than the address you send
> this request from, you may specify "address=<email address>" (no brackets
> around the email address, no quotes!)