------------------------------------------------------------ revno: 6517 committer: Barry Warsaw <[EMAIL PROTECTED]> branch nick: 3.0 timestamp: Sun 2007-07-01 11:51:09 -0400 message: Support for case-preserving addresses. When an Address is given an email address that is not lower cased, the original, case-preserved version is store on the '_original' attribute. The lower-cased version is always used as the key and thus always stored on the 'address' attribute. The IAddress interface is given a new 'original_address' property which returns the case-preserved version. Address's __str__() and __repr__() are similarly modified. The former always includes the case-preserved address; the latter does too, but now also includes the lower-cased 'key' email address (along with the object's id). Searching for an address always does so on the lower-cased version. Test suite is updated as necessary. Also, I'm adding the REPORT_ONLY_FIRST_FAILURE doctest flag so that it's easier to debug doctest failures without having pages of problems to scroll through. modified: Mailman/database/model/address.py Mailman/database/usermanager.py Mailman/docs/addresses.txt Mailman/docs/users.txt Mailman/interfaces/address.py Mailman/testing/test_documentation.py
=== modified file 'Mailman/database/model/address.py' --- a/Mailman/database/model/address.py 2007-06-16 02:37:33 +0000 +++ b/Mailman/database/model/address.py 2007-07-01 15:51:09 +0000 @@ -31,6 +31,7 @@ implements(IAddress) has_field('address', Unicode) + has_field('_original', Unicode) has_field('real_name', Unicode) has_field('verified', Boolean) has_field('registered_on', DateTime) @@ -41,12 +42,26 @@ # Options using_options(shortnames=True) + def __init__(self, address, real_name): + super(Address, self).__init__() + lower_case = address.lower() + self.address = lower_case + self.real_name = real_name + self._original = (None if lower_case == address else address) + def __str__(self): - return formataddr((self.real_name, self.address)) + addr = (self.address if self._original is None else self._original) + return formataddr((self.real_name, addr)) def __repr__(self): - return '<Address: %s [%s]>' % ( - str(self), ('verified' if self.verified else 'not verified')) + verified = ('verified' if self.verified else 'not verified') + address_str = str(self) + if self._original is None: + return '<Address: %s [%s] at %#x>' % ( + address_str, verified, id(self)) + else: + return '<Address: %s [%s] key: %s at %#x>' % ( + address_str, verified, self.address, id(self)) def subscribe(self, mlist, role): from Mailman.database.model import Member @@ -57,3 +72,7 @@ address=self) member.preferences = Preferences() return member + + @property + def original_address(self): + return (self.address if self._original is None else self._original) === modified file 'Mailman/database/usermanager.py' --- a/Mailman/database/usermanager.py 2007-06-16 02:37:33 +0000 +++ b/Mailman/database/usermanager.py 2007-07-01 15:51:09 +0000 @@ -39,7 +39,7 @@ user = User() user.real_name = (real_name if real_name is not None else '') if address: - addrobj = Address(address=address, real_name=user.real_name) + addrobj = Address(address, user.real_name) addrobj.preferences = Preferences() user.link(addrobj) user.preferences = Preferences() @@ -54,16 +54,16 @@ yield user def get_user(self, address): - found = Address.get_by(address=address) + found = Address.get_by(address=address.lower()) return found and found.user def create_address(self, address, real_name=None): - found = Address.get_by(address=address) + found = Address.get_by(address=address.lower()) if found: - raise Errors.ExistingAddressError(address) + raise Errors.ExistingAddressError(found.original_address) if real_name is None: real_name = '' - address = Address(address=address, real_name=real_name) + address = Address(address, real_name) address.preferences = Preferences() return address @@ -75,7 +75,7 @@ address.delete() def get_address(self, address): - return Address.get_by(address=address) + return Address.get_by(address=address.lower()) @property def addresses(self): === modified file 'Mailman/docs/addresses.txt' --- a/Mailman/docs/addresses.txt 2007-06-22 21:15:03 +0000 +++ b/Mailman/docs/addresses.txt 2007-07-01 15:51:09 +0000 @@ -41,6 +41,14 @@ >>> sorted(address.real_name for address in mgr.addresses) ['', '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 <[EMAIL PROTECTED]>' + >>> repr(address_2) + '<Address: Ben Person <[EMAIL PROTECTED]> [not verified] at 0x...>' + You can assign real names to existing addresses. >>> address_1.real_name = 'Anne Person' @@ -156,7 +164,7 @@ [EMAIL PROTECTED] as MemberRole.member> >>> flush() -Now that Elly is both an owner and a member of the mailing list. +Now Elly is both an owner and a member of the mailing list. >>> sorted(mlist.owners.members) [<Member: Elly Person <[EMAIL PROTECTED]> on @@ -174,3 +182,57 @@ [EMAIL PROTECTED] as MemberRole.member>] >>> sorted(mlist.digest_members.members) [] + + +Case-preserved addresses +------------------------ + +Technically speaking, email addresses are case sensitive in the local part. +Mailman preserves the case of addresses and uses the case preserved version +when sending the user a message, but it treats addresses that are different in +case equivalently in all other situations. + + >>> address_6 = mgr.create_address('[EMAIL PROTECTED]', 'Frank Person') + >>> flush() + +The str() of such an address prints the RFC 2822 preferred originator format +with the original case-preserved address. The repr() contains all the gory +details. + + >>> str(address_6) + 'Frank Person <[EMAIL PROTECTED]>' + >>> repr(address_6) + '<Address: Frank Person <[EMAIL PROTECTED]> [not verified] + key: [EMAIL PROTECTED] at 0x...>' + +Both the case-insensitive version of the address and the original +case-preserved version are available on attributes of the IAddress object. + + >>> address_6.address + '[EMAIL PROTECTED]' + >>> address_6.original_address + '[EMAIL PROTECTED]' + +Because addresses are case-insensitive for all other purposes, you cannot +create an address that differs only in case. + + >>> mgr.create_address('[EMAIL PROTECTED]') + Traceback (most recent call last): + ... + ExistingAddressError: [EMAIL PROTECTED] + >>> mgr.create_address('[EMAIL PROTECTED]') + Traceback (most recent call last): + ... + ExistingAddressError: [EMAIL PROTECTED] + >>> mgr.create_address('[EMAIL PROTECTED]') + Traceback (most recent call last): + ... + ExistingAddressError: [EMAIL PROTECTED] + +You can get the address using either the lower cased version or case-preserved +version. In fact, searching for an address is case insensitive. + + >>> mgr.get_address('[EMAIL PROTECTED]').address + '[EMAIL PROTECTED]' + >>> mgr.get_address('[EMAIL PROTECTED]').address + '[EMAIL PROTECTED]' === modified file 'Mailman/docs/users.txt' --- a/Mailman/docs/users.txt 2007-06-22 21:15:03 +0000 +++ b/Mailman/docs/users.txt 2007-07-01 15:51:09 +0000 @@ -48,9 +48,9 @@ address on a user object. >>> user_1.register('[EMAIL PROTECTED]', 'Zoe Person') - <Address: Zoe Person <[EMAIL PROTECTED]> [not verified]> + <Address: Zoe Person <[EMAIL PROTECTED]> [not verified] at 0x...> >>> user_1.register('[EMAIL PROTECTED]') - <Address: [EMAIL PROTECTED] [not verified]> + <Address: [EMAIL PROTECTED] [not verified] at 0x...> >>> flush() >>> sorted(address.address for address in user_1.addresses) ['[EMAIL PROTECTED]', '[EMAIL PROTECTED]'] === modified file 'Mailman/interfaces/address.py' --- a/Mailman/interfaces/address.py 2007-06-15 04:50:40 +0000 +++ b/Mailman/interfaces/address.py 2007-07-01 15:51:09 +0000 @@ -27,6 +27,19 @@ address = Attribute( """Read-only text email address.""") + original_address = Attribute( + """Read-only original case-preserved 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. + + `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. + """) + real_name = Attribute( """Optional real name associated with the email address.""") === modified file 'Mailman/testing/test_documentation.py' --- a/Mailman/testing/test_documentation.py 2007-06-28 03:01:56 +0000 +++ b/Mailman/testing/test_documentation.py 2007-07-01 15:51:09 +0000 @@ -67,7 +67,8 @@ package=Mailman, optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE - | doctest.REPORT_NDIFF), + | doctest.REPORT_NDIFF + | doctest.REPORT_ONLY_FIRST_FAILURE), tearDown=cleaning_teardown) suite.addTest(test) return suite -- (no title) https://code.launchpad.net/~mailman-coders/mailman/3.0 You are receiving this branch notification because you are subscribed to it. To unsubscribe from this branch go to https://code.launchpad.net/~mailman-coders/mailman/3.0/+subscription/mailman-checkins. _______________________________________________ Mailman-checkins mailing list Mailman-checkins@python.org Unsubscribe: http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org