diff options
Diffstat (limited to 'src/mailman/docs/registration.txt')
| -rw-r--r-- | src/mailman/docs/registration.txt | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/src/mailman/docs/registration.txt b/src/mailman/docs/registration.txt new file mode 100644 index 000000000..d243188bc --- /dev/null +++ b/src/mailman/docs/registration.txt @@ -0,0 +1,362 @@ +Address registration +==================== + +When a user wants to join a mailing list -- any mailing list -- in the running +instance, he or she must first register with Mailman. The only thing they +must supply is an email address, although there is additional information they +may supply. All registered email addresses must be verified before Mailman +will send them any list traffic. + + >>> from mailman.app.registrar import Registrar + >>> from mailman.interfaces.registrar import IRegistrar + +The IUserManager manages users, but it does so at a fairly low level. +Specifically, it does not handle verifications, email address syntax validity +checks, etc. The IRegistrar is the interface to the object handling all this +stuff. + +Add a domain, which will provide the context for the verification email +message. + + >>> config.push('mail', """ + ... [domain.mail_example_dot_com] + ... email_host: mail.example.com + ... base_url: http://mail.example.com + ... contact_address: postmaster@mail.example.com + ... """) + + >>> domain = config.domains['mail.example.com'] + +Get a registrar by adapting a context to the interface. + + >>> from zope.interface.verify import verifyObject + >>> registrar = IRegistrar(domain) + >>> verifyObject(IRegistrar, registrar) + True + +Here is a helper function to check the token strings. + + >>> def check_token(token): + ... assert isinstance(token, basestring), 'Not a string' + ... assert len(token) == 40, 'Unexpected length: %d' % len(token) + ... assert token.isalnum(), 'Not alphanumeric' + ... print 'ok' + +Here is a helper function to extract tokens from confirmation messages. + + >>> import re + >>> cre = re.compile('http://mail.example.com/confirm/(.*)') + >>> def extract_token(msg): + ... mo = cre.search(qmsg.get_payload()) + ... return mo.group(1) + + +Invalid email addresses +----------------------- + +The only piece of information you need to register is the email address. +Some amount of sanity checks are performed on the email address, although +honestly, not as much as probably should be done. Still, some patently bad +addresses are rejected outright. + + >>> registrar.register('') + Traceback (most recent call last): + ... + InvalidEmailAddress: '' + >>> registrar.register('some name@example.com') + Traceback (most recent call last): + ... + InvalidEmailAddress: 'some name@example.com' + >>> registrar.register('<script>@example.com') + Traceback (most recent call last): + ... + InvalidEmailAddress: '<script>@example.com' + >>> registrar.register('\xa0@example.com') + Traceback (most recent call last): + ... + InvalidEmailAddress: '\xa0@example.com' + >>> registrar.register('noatsign') + Traceback (most recent call last): + ... + InvalidEmailAddress: 'noatsign' + >>> registrar.register('nodom@ain') + Traceback (most recent call last): + ... + InvalidEmailAddress: 'nodom@ain' + + +Register an email address +------------------------- + +Registration of an unknown address creates nothing until the confirmation step +is complete. No IUser or IAddress is created at registration time, but a +record is added to the pending database, and the token for that record is +returned. + + >>> token = registrar.register(u'aperson@example.com', u'Anne Person') + >>> check_token(token) + ok + +There should be no records in the user manager for this address yet. + + >>> usermgr = config.db.user_manager + >>> print usermgr.get_user(u'aperson@example.com') + None + >>> print usermgr.get_address(u'aperson@example.com') + None + +But this address is waiting for confirmation. + + >>> pendingdb = config.db.pendings + >>> sorted(pendingdb.confirm(token, expunge=False).items()) + [(u'address', u'aperson@example.com'), + (u'real_name', u'Anne Person'), + (u'type', u'registration')] + + +Verification by email +--------------------- + +There is also a verification email sitting in the virgin queue now. This +message is sent to the user in order to verify the registered address. + + >>> switchboard = config.switchboards['virgin'] + >>> len(switchboard.files) + 1 + >>> filebase = switchboard.files[0] + >>> qmsg, qdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: confirm ... + From: confirm-...@mail.example.com + To: aperson@example.com + Message-ID: <...> + Date: ... + Precedence: bulk + <BLANKLINE> + Email Address Registration Confirmation + <BLANKLINE> + Hello, this is the GNU Mailman server at mail.example.com. + <BLANKLINE> + We have received a registration request for the email address + <BLANKLINE> + aperson@example.com + <BLANKLINE> + Before you can start using GNU Mailman at this site, you must first + confirm that this is your email address. You can do this by replying to + this message, keeping the Subject header intact. Or you can visit this + web page + <BLANKLINE> + http://mail.example.com/confirm/... + <BLANKLINE> + If you do not wish to register this email address simply disregard this + message. If you think you are being maliciously subscribed to the list, + or have any other questions, you may contact + <BLANKLINE> + postmaster@mail.example.com + <BLANKLINE> + >>> dump_msgdata(qdata) + _parsemsg : False + nodecorate : True + recips : [u'aperson@example.com'] + reduced_list_headers: True + version : 3 + +The confirmation token shows up in several places, each of which provides an +easy way for the user to complete the confirmation. The token will always +appear in a URL in the body of the message. + + >>> sent_token = extract_token(qmsg) + >>> sent_token == token + True + +The same token will appear in the From header. + + >>> qmsg['from'] == 'confirm-' + token + '@mail.example.com' + True + +It will also appear in the Subject header. + + >>> qmsg['subject'] == 'confirm ' + token + True + +The user would then validate their just registered address by clicking on a +url or responding to the message. Either way, the confirmation process +extracts the token and uses that to confirm the pending registration. + + >>> registrar.confirm(token) + True + +Now, there is an IAddress in the database matching the address, as well as an +IUser linked to this address. The IAddress is verified. + + >>> found_address = usermgr.get_address(u'aperson@example.com') + >>> found_address + <Address: Anne Person <aperson@example.com> [verified] at ...> + >>> found_user = usermgr.get_user(u'aperson@example.com') + >>> found_user + <User "Anne Person" at ...> + >>> found_user.controls(found_address.address) + True + >>> from datetime import datetime + >>> isinstance(found_address.verified_on, datetime) + True + + +Non-standard registrations +-------------------------- + +If you try to confirm a registration token twice, of course only the first one +will work. The second one is ignored. + + >>> token = registrar.register(u'bperson@example.com') + >>> check_token(token) + ok + >>> filebase = switchboard.files[0] + >>> qmsg, qdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> sent_token = extract_token(qmsg) + >>> token == sent_token + True + >>> registrar.confirm(token) + True + >>> registrar.confirm(token) + False + +If an address is in the system, but that address is not linked to a user yet +and the address is not yet validated, then no user is created until the +confirmation step is completed. + + >>> usermgr.create_address(u'cperson@example.com') + <Address: cperson@example.com [not verified] at ...> + >>> token = registrar.register(u'cperson@example.com', u'Claire Person') + >>> print usermgr.get_user(u'cperson@example.com') + None + >>> filebase = switchboard.files[0] + >>> qmsg, qdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> registrar.confirm(token) + True + >>> usermgr.get_user(u'cperson@example.com') + <User "Claire Person" at ...> + >>> usermgr.get_address(u'cperson@example.com') + <Address: cperson@example.com [verified] at ...> + +Even if the address being registered has already been verified, the +registration sends a confirmation. + + >>> token = registrar.register(u'cperson@example.com') + >>> token is not None + True + + +Discarding +---------- + +A confirmation token can also be discarded, say if the user changes his or her +mind about registering. When discarded, no IAddress or IUser is created. + + >>> token = registrar.register(u'eperson@example.com', u'Elly Person') + >>> check_token(token) + ok + >>> registrar.discard(token) + >>> print pendingdb.confirm(token) + None + >>> print usermgr.get_address(u'eperson@example.com') + None + >>> print usermgr.get_user(u'eperson@example.com') + None + + +Registering a new address for an existing user +---------------------------------------------- + +When a new address for an existing user is registered, there isn't too much +different except that the new address will still need to be verified before it +can be used. + + >>> dperson = usermgr.create_user(u'dperson@example.com', u'Dave Person') + >>> dperson + <User "Dave Person" at ...> + >>> address = usermgr.get_address(u'dperson@example.com') + >>> address.verified_on = datetime.now() + + >>> from operator import attrgetter + >>> sorted((addr for addr in dperson.addresses), key=attrgetter('address')) + [<Address: Dave Person <dperson@example.com> [verified] at ...>] + >>> dperson.register(u'david.person@example.com', u'David Person') + <Address: David Person <david.person@example.com> [not verified] at ...> + >>> token = registrar.register(u'david.person@example.com') + >>> filebase = switchboard.files[0] + >>> qmsg, qdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> registrar.confirm(token) + True + >>> user = usermgr.get_user(u'david.person@example.com') + >>> user is dperson + True + >>> user + <User "Dave Person" at ...> + >>> sorted((addr for addr in user.addresses), key=attrgetter('address')) + [<Address: David Person <david.person@example.com> [verified] at ...>, + <Address: Dave Person <dperson@example.com> [verified] at ...>] + + +Corner cases +------------ + +If you try to confirm a token that doesn't exist in the pending database, the +confirm method will just return None. + + >>> registrar.confirm('no token') + False + +Likewise, if you try to confirm, through the IUserRegistrar interface, a token +that doesn't match a registration even, you will get None. However, the +pending even matched with that token will still be removed. + + >>> from mailman.interfaces.pending import IPendable + >>> from zope.interface import implements + + >>> class SimplePendable(dict): + ... implements(IPendable) + >>> pendable = SimplePendable(type='foo', bar='baz') + >>> token = pendingdb.add(pendable) + >>> registrar.confirm(token) + False + >>> print pendingdb.confirm(token) + None + + +Registration and subscription +----------------------------- + +Fred registers with Mailman at the same time that he subscribes to a mailing +list. + + >>> from mailman.app.lifecycle import create_list + >>> mlist = create_list(u'alpha@example.com') + >>> token = registrar.register( + ... u'fred.person@example.com', 'Fred Person', mlist) + +Before confirmation, Fred is not a member of the mailing list. + + >>> print mlist.members.get_member(u'fred.person@example.com') + None + +But after confirmation, he is. + + >>> registrar.confirm(token) + True + >>> print mlist.members.get_member(u'fred.person@example.com') + <Member: Fred Person <fred.person@example.com> + on alpha@example.com as MemberRole.member> + + +Clean up +-------- + + >>> config.pop('mail') |
