Commit 3f1f5a28 authored by Barry Warsaw's avatar Barry Warsaw

Because it was just to damn confusing, rename IAddress.address to

IAddress.email and IAddress.original_address to IAddress.original_email.  From
now on we'll use "address" to talk about the IAddress object and "email" to
talk about the textual email address.
parent d0f8e9e0
......@@ -77,20 +77,27 @@ You can also specify a list of owner email addresses. If these addresses are
not yet known, they will be registered, and new users will be linked to them.
However the addresses are not verified.
>>> owners = ['aperson@example.com', 'bperson@example.com',
... 'cperson@example.com', 'dperson@example.com']
>>> owners = [
... 'aperson@example.com',
... 'bperson@example.com',
... 'cperson@example.com',
... 'dperson@example.com',
... ]
>>> mlist_2 = create_list('test_2@example.com', owners)
>>> print mlist_2.fqdn_listname
test_2@example.com
>>> print mlist_2.msg_footer
test footer
>>> sorted(addr.address for addr in mlist_2.owners.addresses)
[u'aperson@example.com', u'bperson@example.com',
u'cperson@example.com', u'dperson@example.com']
>>> dump_list(address.email for address in mlist_2.owners.addresses)
aperson@example.com
bperson@example.com
cperson@example.com
dperson@example.com
None of the owner addresses are verified.
>>> any(addr.verified_on is not None for addr in mlist_2.owners.addresses)
>>> any(address.verified_on is not None
... for address in mlist_2.owners.addresses)
False
However, all addresses are linked to users.
......@@ -117,8 +124,11 @@ the system, they won't be created again.
>>> user_d.real_name = 'Dirk Person'
>>> mlist_3 = create_list('test_3@example.com', owners)
>>> sorted(user.real_name for user in mlist_3.owners.users)
[u'Anne Person', u'Bart Person', u'Caty Person', u'Dirk Person']
>>> dump_list(user.real_name for user in mlist_3.owners.users)
Anne Person
Bart Person
Caty Person
Dirk Person
Deleting a list
......@@ -139,6 +149,8 @@ artifacts.
We should now be able to completely recreate the mailing list.
>>> mlist_2a = create_list('test_2@example.com', owners)
>>> sorted(addr.address for addr in mlist_2a.owners.addresses)
[u'aperson@example.com', u'bperson@example.com',
u'cperson@example.com', u'dperson@example.com']
>>> dump_list(address.email for address in mlist_2a.owners.addresses)
aperson@example.com
bperson@example.com
cperson@example.com
dperson@example.com
......@@ -53,15 +53,15 @@ class Registrar:
implements(IRegistrar)
def register(self, mlist, address, real_name=None):
def register(self, mlist, email, real_name=None):
"""See `IUserRegistrar`."""
# First, do validation on the email address. If the address is
# invalid, it will raise an exception, otherwise it just returns.
validate(address)
validate(email)
# Create a pendable for the registration.
pendable = PendableRegistration(
type=PendableRegistration.PEND_KEY,
address=address,
email=email,
real_name=real_name)
pendable['list_name'] = mlist.fqdn_listname
token = getUtility(IPendings).add(pendable)
......@@ -74,12 +74,12 @@ class Registrar:
confirm_address = mlist.confirm_address(token)
# For i18n interpolation.
confirm_url = mlist.domain.confirm_url(token)
email_address = address
email_address = email
domain_name = mlist.domain.email_host
contact_address = mlist.domain.contact_address
# Send a verification email to the address.
text = _(resource_string('mailman.templates.en', 'verify.txt'))
msg = UserNotification(address, confirm_address, subject, text)
msg = UserNotification(email, confirm_address, subject, text)
msg.send(mlist)
return token
......@@ -90,7 +90,7 @@ class Registrar:
if pendable is None:
return False
missing = object()
address = pendable.get('address', missing)
email = pendable.get('email', missing)
real_name = pendable.get('real_name', missing)
list_name = pendable.get('list_name', missing)
if pendable.get('type') != PendableRegistration.PEND_KEY:
......@@ -104,41 +104,41 @@ class Registrar:
# and an IUser linked to this IAddress. See if any of these objects
# currently exist in our database.
user_manager = getUtility(IUserManager)
addr = (user_manager.get_address(address)
if address is not missing else None)
user = (user_manager.get_user(address)
if address is not missing else None)
address = (user_manager.get_address(email)
if email is not missing else None)
user = (user_manager.get_user(email)
if email is not missing else None)
# If there is neither an address nor a user matching the confirmed
# record, then create the user, which will in turn create the address
# and link the two together
if addr is None:
if address is None:
assert user is None, 'How did we get a user but not an address?'
user = user_manager.create_user(address, real_name)
user = user_manager.create_user(email, real_name)
# Because the database changes haven't been flushed, we can't use
# IUserManager.get_address() to find the IAddress just created
# under the hood. Instead, iterate through the IUser's addresses,
# of which really there should be only one.
for addr in user.addresses:
if addr.address == address:
for address in user.addresses:
if address.email == email:
break
else:
raise AssertionError('Could not find expected IAddress')
elif user is None:
user = user_manager.create_user()
user.real_name = real_name
user.link(addr)
user.link(address)
else:
# The IAddress and linked IUser already exist, so all we need to
# do is verify the address.
pass
addr.verified_on = datetime.datetime.now()
address.verified_on = datetime.datetime.now()
# If this registration is tied to a mailing list, subscribe the person
# to the list right now.
list_name = pendable.get('list_name')
if list_name is not None:
mlist = getUtility(IListManager).get(list_name)
if mlist:
addr.subscribe(mlist, MemberRole.member)
address.subscribe(mlist, MemberRole.member)
return True
def discard(self, token):
......
......@@ -156,21 +156,21 @@ class Members:
if len(addresses) == 0:
print >> fp, mlist.fqdn_listname, 'has no members'
return
for address in sorted(addresses, key=attrgetter('address')):
for address in sorted(addresses, key=attrgetter('email')):
if args.regular:
member = mlist.members.get_member(address.address)
member = mlist.members.get_member(address.email)
if member.delivery_mode != DeliveryMode.regular:
continue
if args.digest is not None:
member = mlist.members.get_member(address.address)
member = mlist.members.get_member(address.email)
if member.delivery_mode not in digest_types:
continue
if args.nomail is not None:
member = mlist.members.get_member(address.address)
member = mlist.members.get_member(address.email)
if member.delivery_status not in status_types:
continue
print >> fp, formataddr(
(address.real_name, address.original_address))
(address.real_name, address.original_email))
finally:
if fp is not sys.stdout:
fp.close()
......
......@@ -77,8 +77,8 @@ Setting the owner
By default, no list owners are specified.
>>> print list(mlist.owners.addresses)
[]
>>> dump_list(mlist.owners.addresses)
*Empty*
But you can specify an owner address on the command line when you create the
mailing list.
......@@ -91,8 +91,8 @@ mailing list.
Created mailing list: test4@example.com
>>> mlist = list_manager.get('test4@example.com')
>>> print list(mlist.owners.addresses)
[<Address: foo@example.org [not verified] at ...>]
>>> dump_list(repr(address) for address in mlist.owners.addresses)
<Address: foo@example.org [not verified] at ...>
You can even specify more than one address for the owners.
::
......@@ -104,10 +104,10 @@ You can even specify more than one address for the owners.
>>> mlist = list_manager.get('test5@example.com')
>>> from operator import attrgetter
>>> print sorted(mlist.owners.addresses, key=attrgetter('address'))
[<Address: bar@example.net [not verified] at ...>,
<Address: baz@example.net [not verified] at ...>,
<Address: foo@example.net [not verified] at ...>]
>>> dump_list(repr(address) for address in mlist.owners.addresses)
<Address: bar@example.net [not verified] at ...>
<Address: baz@example.net [not verified] at ...>
<Address: foo@example.net [not verified] at ...>
Setting the language
......
......@@ -205,8 +205,6 @@ need a file containing email addresses and full names that can be parsed by
::
>>> mlist2 = create_list('test2@example.com')
>>> addresses = [
... ]
>>> import os
>>> path = os.path.join(config.VAR_DIR, 'addresses.txt')
......@@ -221,8 +219,11 @@ need a file containing email addresses and full names that can be parsed by
>>> args.listname = [mlist2.fqdn_listname]
>>> command.process(args)
>>> sorted(address.address for address in mlist2.members.addresses)
[u'aperson@example.com', u'bperson@example.com', u'cperson@example.com']
>>> from operator import attrgetter
>>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
aperson@example.com
Bart Person <bperson@example.com>
Cate Person <cperson@example.com>
You can also specify ``-`` as the filename, in which case the addresses are
taken from standard input.
......@@ -244,9 +245,13 @@ taken from standard input.
>>> command.process(args)
>>> sys.stdin = sys.__stdin__
>>> sorted(address.address for address in mlist2.members.addresses)
[u'aperson@example.com', u'bperson@example.com', u'cperson@example.com',
u'dperson@example.com', u'eperson@example.com', u'fperson@example.com']
>>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
aperson@example.com
Bart Person <bperson@example.com>
Cate Person <cperson@example.com>
dperson@example.com
Elly Person <eperson@example.com>
Fred Person <fperson@example.com>
Blank lines and lines that begin with '#' are ignored.
::
......@@ -262,10 +267,15 @@ Blank lines and lines that begin with '#' are ignored.
>>> args.input_filename = path
>>> command.process(args)
>>> sorted(address.address for address in mlist2.members.addresses)
[u'aperson@example.com', u'bperson@example.com', u'cperson@example.com',
u'dperson@example.com', u'eperson@example.com', u'fperson@example.com',
u'gperson@example.com', u'iperson@example.com']
>>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
aperson@example.com
Bart Person <bperson@example.com>
Cate Person <cperson@example.com>
dperson@example.com
Elly Person <eperson@example.com>
Fred Person <fperson@example.com>
gperson@example.com
iperson@example.com
Addresses which are already subscribed are ignored, although a warning is
printed.
......@@ -282,10 +292,16 @@ printed.
Already subscribed (skipping): gperson@example.com
Already subscribed (skipping): aperson@example.com
>>> sorted(address.address for address in mlist2.members.addresses)
[u'aperson@example.com', u'bperson@example.com', u'cperson@example.com',
u'dperson@example.com', u'eperson@example.com', u'fperson@example.com',
u'gperson@example.com', u'iperson@example.com', u'jperson@example.com']
>>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
aperson@example.com
Bart Person <bperson@example.com>
Cate Person <cperson@example.com>
dperson@example.com
Elly Person <eperson@example.com>
Fred Person <fperson@example.com>
gperson@example.com
iperson@example.com
jperson@example.com
Displaying members
......
......@@ -281,7 +281,7 @@ to unsubscribe Anne from the alpha mailing list.
>>> print unicode(results)
The results of your email command are provided below.
<BLANKLINE>
Invalid or unverified address: anne.person@example.org
Invalid or unverified email address: anne.person@example.org
<BLANKLINE>
>>> print mlist.members.get_member('anne@example.com')
......
......@@ -144,36 +144,36 @@ class Leave:
def process(self, mlist, msg, msgdata, arguments, results):
"""See `IEmailCommand`."""
address = msg.sender
if not address:
email = msg.sender
if not email:
print >> results, _(
'$self.name: No valid address found to unsubscribe')
'$self.name: No valid email address found to unsubscribe')
return ContinueProcessing.no
user_manager = getUtility(IUserManager)
user = user_manager.get_user(address)
user = user_manager.get_user(email)
if user is None:
print >> results, _('No registered user for address: $address')
print >> results, _('No registered user for email address: $email')
return ContinueProcessing.no
# The address that the -leave command was sent from, must be verified.
# Otherwise you could link a bogus address to anyone's account, and
# then send a leave command from that address.
if user_manager.get_address(address).verified_on is None:
print >> results, _('Invalid or unverified address: $address')
if user_manager.get_address(email).verified_on is None:
print >> results, _('Invalid or unverified email address: $email')
return ContinueProcessing.no
for user_address in user.addresses:
# Only recognize verified addresses.
if user_address.verified_on is None:
continue
member = mlist.members.get_member(user_address.address)
member = mlist.members.get_member(user_address.email)
if member is not None:
break
else:
# None of the user's addresses are subscribed to this mailing list.
print >> results, _(
'$self.name: $address is not a member of $mlist.fqdn_listname')
'$self.name: $email is not a member of $mlist.fqdn_listname')
return ContinueProcessing.no
member.unsubscribe()
person = formataddr((user.real_name, address))
person = formataddr((user.real_name, email))
print >> results, _('$person left $mlist.fqdn_listname')
return ContinueProcessing.yes
......
......@@ -23,7 +23,7 @@ CREATE INDEX ix_acceptablealias_alias
CREATE TABLE address (
id INTEGER NOT NULL,
address TEXT,
email TEXT,
_original TEXT,
real_name TEXT,
verified_on TIMESTAMP,
......
......@@ -60,20 +60,20 @@ class InvalidEmailAddressError(AddressError):
class IAddress(Interface):
"""Email address related information."""
address = Attribute(
email = Attribute(
"""Read-only text email address.""")
original_address = Attribute(
"""Read-only original case-preserved address.
original_email = Attribute(
"""Read-only original case-preserved email address.
For almost all intents and purposes, addresses in Mailman are case
insensitive, however because RFC 2821 allows for case sensitive local
parts, Mailman preserves the case of the original address when
emailing the user.
For almost all intents and purposes, email addresses in Mailman are
case insensitive, however because RFC 2821 allows for case sensitive
local parts, Mailman preserves the case of the original email address
when delivering a message to the user.
`original_address` will be the same as address if the original address
was all lower case. Otherwise `original_address` will be the case
preserved address; `address` will always be lower case.
`original_email` will be the same as `email` if the original email
address was all lower case. Otherwise `original_email` will be the
case preserved email address; `email` will always be lower case.
""")
real_name = Attribute(
......
......@@ -35,28 +35,29 @@ from zope.interface import Interface
class IRegistrar(Interface):
"""Interface for registering and verifying addresses and users.
"""Interface for registering and verifying email addresses and users.
This is a higher level interface to user registration, address
This is a higher level interface to user registration, email address
confirmation, etc. than the IUserManager. The latter does no validation,
syntax checking, or confirmation, while this interface does.
"""
def register(mlist, address, real_name=None):
def register(mlist, email, real_name=None):
"""Register the email address, requesting verification.
No IAddress or IUser is created during this step, but after successful
confirmation, it is guaranteed that an IAddress with a linked IUser
will exist. When a verified IAddress matching address already exists,
this method will do nothing, except link a new IUser to the IAddress
if one is not yet associated with the address.
No `IAddress` or `IUser` is created during this step, but after
successful confirmation, it is guaranteed that an `IAddress` with a
linked `IUser` will exist. When a verified `IAddress` matching
`email` already exists, this method will do nothing, except link a new
`IUser` to the `IAddress` if one is not yet associated with the
email address.
In all cases, the email address is sanity checked for validity first.
:param mlist: The mailing list that is the focus of this registration.
:type mlist: `IMailingList`
:param address: The email address to register.
:type address: str
:param email: The email address to register.
:type email: str
:param real_name: The optional real name of the user.
:type real_name: str
:return: The confirmation token string.
......
......@@ -33,29 +33,31 @@ class IUser(Interface):
"""A basic user."""
real_name = Attribute(
"""This user's Real Name.""")
"""This user's real name.""")
password = Attribute(
"""This user's password information.""")
addresses = Attribute(
"""An iterator over all the IAddresses controlled by this user.""")
"""An iterator over all the `IAddresses` controlled by this user.""")
memberships = Attribute(
"""A roster of this user's memberships.""")
def register(address, real_name=None):
def register(email, real_name=None):
"""Register the given email address and link it to this user.
In this case, 'address' is a text email address, not an IAddress
object. If real_name is not given, the empty string is used.
Raises AddressAlreadyLinkedError if this IAddress is already linked to
another user. If the corresponding IAddress already exists but is not
linked, then it is simply linked to the user, in which case
real_name is ignored.
Return the new IAddress object.
:param email: The text email address to register.
:type email: str
:param real_name: The user's real name. If not given the empty string
is used.
:type real_name: str
:return: The address object linked to the user. If the associated
address object already existed (unlinked to a user) then the
`real_name` parameter is ignored.
:rtype: `IAddress`
:raises AddressAlreadyLinkedError: if this `IAddress` is already
linked to another user.
"""
def link(address):
......@@ -73,11 +75,13 @@ class IUser(Interface):
some other user.
"""
def controls(address):
def controls(email):
"""Determine whether this user controls the given email address.
'address' is a text email address. This method returns true if the
user controls the given email address, otherwise false.
:param email: The text email address to register.
:type email: str
:return: True if the user controls the given email address.
:rtype: bool
"""
preferences = Attribute(
......
......@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
"""Interface describing a user manager service."""
"""Interface describing the user management service."""
from __future__ import absolute_import, unicode_literals
......@@ -30,67 +30,74 @@ from zope.interface import Interface, Attribute
class IUserManager(Interface):
"""The interface of a global user manager service.
Different user managers have different concepts of what a user is, and the
users managed by different IUserManagers are completely independent. This
is how you can separate the user contexts for different domains, on a
multiple domain system.
There is one special roster, the null roster ('') which contains all
IUsers in all IRosters.
"""
def create_user(address=None, real_name=None):
"""Create and return an IUser.
When address is given, an IAddress is also created and linked to the
new IUser object. If the address already exists, an
`ExistingAddressError` is raised. If the address exists but is
already linked to another user, an AddressAlreadyLinkedError is
raised.
When real_name is given, the IUser's real_name is set to this string.
If an IAddress is also created and linked, its real_name is set to the
same string.
"""The global user management service."""
def create_user(email=None, real_name=None):
"""Create and return an `IUser`.
:param email: The text email address for the user being created.
:type email: str
:param real_name: The real name of the user.
:type real_name: str
:return: The newly created user, with the given email address and real
name, if given.
:rtype: `IUser`
:raises ExistingAddressError: when the email address is already
registered.
"""
def delete_user(user):
"""Delete the given IUser."""
"""Delete the given user.
def get_user(address):
:param user: The user to delete.
:type user: `IUser`.
"""
def get_user(email):
"""Get the user that controls the given email address, or None.
'address' is a text email address.
:param email: The email address to look up.
:type email: str
:return: The user found or None.
:rtype: `IUser`.
"""
users = Attribute(
"""An iterator over all the IUsers managed by this user manager.""")
def create_address(address, real_name=None):
"""Create and return an unlinked IAddress object.
address is the text email address. If real_name is not given, it
defaults to the empty string. If the IAddress already exists an
ExistingAddressError is raised.
"""An iterator over all the `IUsers` managed by this user manager.""")
def create_address(email, real_name=None):
"""Create and return an address unlinked to any user.
:param email: The text email address for the address being created.
:type email: str
:param real_name: The real name associated with the address.
:type real_name: str
:return: The newly created address object, with the given email
address and real name, if given.
:rtype: `IAddress`
:raises ExistingAddressError: when the email address is already
registered.
"""
def delete_address(address):
"""Delete the given IAddress object.
"""Delete the given `IAddress` object.
If the `IAddress` is linked to a user, it is first unlinked before it
is deleted.
If this IAddress linked to a user, it is first unlinked before it is
deleted.
:param address: The address to delete.
:type address: `IAddress`.
"""
def get_address(address):
"""Find and return the `IAddress` matching a text address.
def get_address(email):
"""Find and return the `IAddress` matching an email address.
:param address: the text email address
:type address: string
:param email: The text email address.
:type email: str
:return: The matching `IAddress` object, or None if no registered
`IAddress` matches the text address
`IAddress` matches the text address.
:rtype: `IAddress` or None
"""
addresses = Attribute(
"""An iterator over all the IAddresses managed by this manager.""")
"""An iterator over all the `IAddresses` managed by this manager.""")
......@@ -41,7 +41,7 @@ class Address(Model):
implements(IAddress)
id = Int(primary=True)
address = Unicode()
email = Unicode()
_original = Unicode()
real_name = Unicode()
verified_on = DateTime()
......@@ -52,15 +52,15 @@ class Address(Model):
preferences_id = Int()
preferences = Reference(preferences_id, 'Preferences.id')
def __init__(self, address, real_name):
def __init__(self, email, real_name):
super(Address, self).__init__()
lower_case = address.lower()
self.address = lower_case
lower_case = email.lower()
self.email = lower_case
self.real_name = real_name
self._original = (None if lower_case == address else address)
self._original = (None if lower_case == email else email)
def __str__(self):
addr = (self.address if self._original is None else self._original)
addr = (self.email if self._original is None else self._original)
return formataddr((self.real_name, addr))
def __repr__(self):
......@@ -71,7 +71,7 @@ class Address(Model):
address_str, verified, id(self))
else:
return '<Address: {0} [{1}] key: {2} at {3:#x}>'.format(
address_str, verified, self.address, id(self))
address_str, verified, self.email, id(self))
def subscribe(self, mailing_list, role):
# This member has no preferences by default.
......@@ -83,7 +83,7 @@ class Address(Model):
Member.address == self).one()
if member:
raise AlreadySubscribedError(
mailing_list.fqdn_listname, self.address, role)
mailing_list.fqdn_listname, self.email, role)
member = Member(role=role,
mailing_list=mailing_list.fqdn_listname,
address=self)
......@@ -92,5 +92,5 @@ class Address(Model):
return member
@property
def original_address(self):
return (self.address if self._original is None else self._original)
def original_email(self):
return (self.email if self._original is None else self._original)
......@@ -18,42 +18,45 @@ Creating addresses
Addresses are created directly through the user manager, which starts out with
no addresses.
>>> sorted(address.address for address in user_manager.addresses)
[]
>>> dump_list(address.email for address in user_manager.addresses)
*Empty*
Creating an unlinked email address is straightforward.
>>> address_1 = user_manager.create_address('aperson@example.com')
>>> sorted(address.address for address in user_manager.addresses)
[u'aperson@example.com']
>>> dump_list(address.email for address in user_manager.addresses)
aperson@example.com
However, such addresses have no real name.
>>> address_1.real_name
u''
>>> print address_1.real_name
<BLANKLINE>
You can also create an email address object with a real name.
>>> address_2 = user_manager.create_address(
... 'bperson@example.com', 'Ben Person')
>>> sorted(address.address for address in user_manager.addresses)
[u'aperson@example.com', u'bperson@example.com']
>>> sorted(address.real_name for address in user_manager.addresses)
[u'', u'Ben Person']
>>> dump_list(address.email for address in user_manager.addresses)
aperson@example.com
bperson@example.com
>>> dump_list(address.real_name for address in user_manager.addresses)
<BLANKLINE>
Ben Person
The ``str()`` of the address is the RFC 2822 preferred originator format,
while the ``repr()`` carries more information.
>>> str(address_2)
'Ben Person <bperson@example.com>'
>>> repr(address_2)