summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/bounces.py2
-rw-r--r--src/mailman/app/moderator.py6
-rw-r--r--src/mailman/archiving/mailarchive.py2
-rw-r--r--src/mailman/archiving/mhonarc.py2
-rw-r--r--src/mailman/archiving/prototype.py2
-rw-r--r--src/mailman/commands/eml_membership.py2
-rw-r--r--src/mailman/docs/NEWS.rst12
-rw-r--r--src/mailman/email/message.py2
-rw-r--r--src/mailman/email/tests/test_message.py15
-rw-r--r--src/mailman/model/bounce.py2
-rw-r--r--src/mailman/model/messagestore.py3
-rw-r--r--src/mailman/rest/addresses.py11
-rw-r--r--src/mailman/rest/docs/addresses.rst5
-rw-r--r--src/mailman/rest/docs/users.rst4
-rw-r--r--src/mailman/rest/tests/test_addresses.py174
-rw-r--r--src/mailman/rest/users.py151
-rw-r--r--src/mailman/rules/approved.py3
-rw-r--r--src/mailman/rules/implicit_dest.py2
-rw-r--r--src/mailman/rules/tests/test_approved.py32
-rw-r--r--src/mailman/utilities/email.py2
20 files changed, 390 insertions, 44 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py
index 34b3784c5..b0a316ad6 100644
--- a/src/mailman/app/bounces.py
+++ b/src/mailman/app/bounces.py
@@ -202,7 +202,7 @@ def send_probe(member, msg):
)
message_id = msg['message-id']
if isinstance(message_id, bytes):
- message_id = message_id.decode("ascii")
+ message_id = message_id.decode('ascii')
pendable = _ProbePendable(
# We can only pend unicodes.
member_id=member.member_id.hex,
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index 6ada9249f..105e53617 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -86,9 +86,9 @@ def hold_message(mlist, msg, msgdata=None, reason=None):
# Message-ID header.
message_id = msg.get('message-id')
if message_id is None:
- msg['Message-ID'] = message_id = make_msgid().decode("ascii")
- if isinstance(message_id, bytes):
- message_id = message_id.decode("ascii")
+ msg['Message-ID'] = message_id = make_msgid().decode('ascii')
+ elif isinstance(message_id, bytes):
+ message_id = message_id.decode('ascii')
getUtility(IMessageStore).add(msg)
# Prepare the message metadata with some extra information needed only by
# the moderation interface.
diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py
index f22eb7fb2..c5fe5d0cb 100644
--- a/src/mailman/archiving/mailarchive.py
+++ b/src/mailman/archiving/mailarchive.py
@@ -69,7 +69,7 @@ class MailArchive:
if message_id_hash is None:
return None
if isinstance(message_id_hash, bytes):
- message_id_hash = message_id_hash.decode("ascii")
+ message_id_hash = message_id_hash.decode('ascii')
return urljoin(self.base_url, message_id_hash)
def archive_message(self, mlist, msg):
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index e773860e3..f2d1f77fe 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -74,7 +74,7 @@ class MHonArc:
if message_id_hash is None:
return None
if isinstance(message_id_hash, bytes):
- message_id_hash = message_id_hash.decode("ascii")
+ message_id_hash = message_id_hash.decode('ascii')
return urljoin(self.list_url(mlist), message_id_hash)
def archive_message(self, mlist, msg):
diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py
index 6ef781c0c..77b2294ed 100644
--- a/src/mailman/archiving/prototype.py
+++ b/src/mailman/archiving/prototype.py
@@ -69,7 +69,7 @@ class Prototype:
if message_id_hash is None:
return None
if isinstance(message_id_hash, bytes):
- message_id_hash = message_id_hash.decode("ascii")
+ message_id_hash = message_id_hash.decode('ascii')
return urljoin(Prototype.list_url(mlist), message_id_hash)
@staticmethod
diff --git a/src/mailman/commands/eml_membership.py b/src/mailman/commands/eml_membership.py
index d88406ae8..c56b14041 100644
--- a/src/mailman/commands/eml_membership.py
+++ b/src/mailman/commands/eml_membership.py
@@ -73,7 +73,7 @@ used.
file=results)
return ContinueProcessing.no
if isinstance(address, bytes):
- address = address.decode("ascii")
+ address = address.decode('ascii')
# Have we already seen one join request from this user during the
# processing of this email?
joins = getattr(results, 'joins', set())
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index ac81cd386..9b37ca7e8 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -16,9 +16,13 @@ Bugs
----
* Fixed Unicode errors in the digest runner and when sending messages to the
site owner as a fallback. Given by Aurélien Bompard. (LP: #1130957).
- * Fix Unicode errors when a message being added to the digest has non-ascii
+ * Fixed Unicode errors when a message being added to the digest has non-ascii
characters in its payload, but no Content-Type header defining a charset.
Given by Aurélien Bompard. (LP: #1170347)
+ * Fixed messages without a `text/plain` part crashing the `Approved` rule.
+ Given by Aurélien Bompard. (LP: #1158721)
+ * Fixed getting non-ASCII filenames from RFC 2231 i18n'd messages. Given by
+ Aurélien Bompard. (LP: #1060951)
Commands
--------
@@ -61,6 +65,12 @@ REST
internal change only.
* The JSON representation `http_etag` key uses an algorithm that is
insensitive to Python's dictionary sort order.
+ * The address resource now has an additional '/user' sub-resource which can
+ be used to GET the address's linked user if there is one. This
+ sub-resource also supports POST to link an unlinked address (with an
+ optional 'auto_create' flag), and PUT to link the address to a different
+ user. It also supports DELETE to unlink the address. (LP: #1312884)
+ Given by Aurélien Bompard based on work by Nicolas Karageuzian.
3.0 beta 4 -- "Time and Motion"
diff --git a/src/mailman/email/message.py b/src/mailman/email/message.py
index 6ddedc48e..24c6ead9e 100644
--- a/src/mailman/email/message.py
+++ b/src/mailman/email/message.py
@@ -127,7 +127,7 @@ class Message(email.message.Message):
if not sender:
continue
if isinstance(sender, bytes):
- sender = sender.decode("ascii")
+ sender = sender.decode('ascii')
clean_senders.append(sender)
return clean_senders
diff --git a/src/mailman/email/tests/test_message.py b/src/mailman/email/tests/test_message.py
index 122562d0a..1fdef5e86 100644
--- a/src/mailman/email/tests/test_message.py
+++ b/src/mailman/email/tests/test_message.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TestMessage',
+ 'TestMessageSubclass',
]
@@ -29,7 +30,7 @@ import unittest
from email.parser import FeedParser
from mailman.app.lifecycle import create_list
-from mailman.email.message import UserNotification, Message
+from mailman.email.message import Message, UserNotification
from mailman.testing.helpers import get_queue_messages
from mailman.testing.layers import ConfigLayer
@@ -57,16 +58,16 @@ class TestMessage(unittest.TestCase):
self._msg.send(self._mlist)
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
- self.assertEqual(messages[0].msg.get_all('precedence'),
+ self.assertEqual(messages[0].msg.get_all('precedence'),
['omg wtf bbq'])
class TestMessageSubclass(unittest.TestCase):
-
def test_i18n_filenames(self):
parser = FeedParser(_factory=Message)
- parser.feed(b"""Message-ID: <blah@example.com>
+ parser.feed(b"""\
+Message-ID: <blah@example.com>
Content-Type: multipart/mixed; boundary="------------050607040206050605060208"
This is a multi-part message in MIME format.
@@ -78,15 +79,15 @@ Test message containing an attachment with an accented filename
--------------050607040206050605060208
Content-Disposition: attachment;
- filename*=UTF-8''d%C3%A9jeuner.txt
+ filename*=UTF-8''d%C3%A9jeuner.txt
Test content
--------------050607040206050605060208--
""")
msg = parser.close()
- attachment = msg.get_payload()[1]
+ attachment = msg.get_payload(1)
try:
filename = attachment.get_filename()
except TypeError as e:
self.fail(e)
- self.assertEqual(filename, u"d\xe9jeuner.txt")
+ self.assertEqual(filename, u'd\xe9jeuner.txt')
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py
index 29a35266e..26ebbe0c6 100644
--- a/src/mailman/model/bounce.py
+++ b/src/mailman/model/bounce.py
@@ -59,7 +59,7 @@ class BounceEvent(Model):
self.timestamp = now()
msgid = msg['message-id']
if isinstance(msgid, bytes):
- msgid = msgid.decode("ascii")
+ msgid = msgid.decode('ascii')
self.message_id = msgid
self.context = (BounceContext.normal if context is None else context)
self.processed = False
diff --git a/src/mailman/model/messagestore.py b/src/mailman/model/messagestore.py
index 3b8aed9c5..12b2aef46 100644
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -24,6 +24,7 @@ __all__ = [
'MessageStore',
]
+
import os
import errno
import base64
@@ -59,7 +60,7 @@ class MessageStore:
# Calculate and insert the X-Message-ID-Hash.
message_id = message_ids[0]
if isinstance(message_id, bytes):
- message_id = message_id.decode("ascii")
+ message_id = message_id.decode('ascii')
# Complain if the Message-ID already exists in the storage.
existing = store.query(Message).filter(
Message.message_id == message_id).first()
diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py
index fa3d099b6..f8516bc37 100644
--- a/src/mailman/rest/addresses.py
+++ b/src/mailman/rest/addresses.py
@@ -62,6 +62,9 @@ class _AddressBase(CollectionMixin):
representation['display_name'] = address.display_name
if address.verified_on:
representation['verified_on'] = address.verified_on
+ if address.user:
+ representation['user'] = path_to(
+ 'users/{0}'.format(address.user.user_id.int))
return representation
def _get_collection(self, request):
@@ -156,6 +159,14 @@ class AnAddress(_AddressBase):
child = _VerifyResource(self._address, 'unverify')
return child, []
+ @child()
+ def user(self, request, segments):
+ """/addresses/<email>/user"""
+ if self._address is None:
+ return NotFound(), []
+ # Avoid circular imports.
+ from mailman.rest.users import AddressUser
+ return AddressUser(self._address)
class UserAddresses(_AddressBase):
diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst
index fec0c194b..8d7ca6835 100644
--- a/src/mailman/rest/docs/addresses.rst
+++ b/src/mailman/rest/docs/addresses.rst
@@ -161,6 +161,7 @@ addresses live in the /addresses namespace.
original_email: dave@example.com
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/dave@example.com
+ user: http://localhost:9001/3.0/users/1
http_etag: "..."
start: 0
total_size: 1
@@ -172,6 +173,7 @@ addresses live in the /addresses namespace.
original_email: dave@example.com
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/dave@example.com
+ user: http://localhost:9001/3.0/users/1
A user can be associated with multiple email addresses. You can add new
addresses to an existing user.
@@ -208,6 +210,7 @@ The user controls these new addresses.
original_email: dave.person@example.org
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/dave.person@example.org
+ user: http://localhost:9001/3.0/users/1
entry 1:
display_name: Dave Person
email: dave@example.com
@@ -215,6 +218,7 @@ The user controls these new addresses.
original_email: dave@example.com
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/dave@example.com
+ user: http://localhost:9001/3.0/users/1
entry 2:
display_name: Davie P
email: dp@example.org
@@ -222,6 +226,7 @@ The user controls these new addresses.
original_email: dp@example.org
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/dp@example.org
+ user: http://localhost:9001/3.0/users/1
http_etag: "..."
start: 0
total_size: 3
diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst
index 04533f578..b2adcaccb 100644
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -329,18 +329,21 @@ order by original (i.e. case-preserved) email address.
registered_on: 2005-08-01T07:49:23
self_link:
http://localhost:9001/3.0/addresses/fred.q.person@example.com
+ user: http://localhost:9001/3.0/users/6
entry 1:
email: fperson@example.com
http_etag: "..."
original_email: fperson@example.com
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/fperson@example.com
+ user: http://localhost:9001/3.0/users/6
entry 2:
email: fred.person@example.com
http_etag: "..."
original_email: fred.person@example.com
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/fred.person@example.com
+ user: http://localhost:9001/3.0/users/6
entry 3:
display_name: Fred Person
email: fred@example.com
@@ -348,6 +351,7 @@ order by original (i.e. case-preserved) email address.
original_email: fred@example.com
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/fred@example.com
+ user: http://localhost:9001/3.0/users/6
http_etag: "..."
start: 0
total_size: 4
diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py
index f4aeb3013..bbdd7d763 100644
--- a/src/mailman/rest/tests/test_addresses.py
+++ b/src/mailman/rest/tests/test_addresses.py
@@ -206,3 +206,177 @@ class TestAddresses(unittest.TestCase):
'email': 'anne.person@example.org',
})
self.assertEqual(cm.exception.code, 404)
+
+ def test_address_with_user(self):
+ # An address which is already linked to a user has a 'user' key in the
+ # JSON representation.
+ with transaction():
+ getUtility(IUserManager).create_user('anne@example.com')
+ json, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com')
+ self.assertEqual(headers['status'], '200')
+ self.assertEqual(json['user'], 'http://localhost:9001/3.0/users/1')
+
+ def test_address_without_user(self):
+ # The 'user' key is missing from the JSON representation of an address
+ # with no linked user.
+ with transaction():
+ getUtility(IUserManager).create_address('anne@example.com')
+ json, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com')
+ self.assertEqual(headers['status'], '200')
+ self.assertNotIn('user', json)
+
+ def test_user_subresource_on_unlinked_address(self):
+ # Trying to access the 'user' subresource on an address that is not
+ # linked to a user will return a 404 error.
+ with transaction():
+ getUtility(IUserManager).create_address('anne@example.com')
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_user_subresource(self):
+ # For an address which is linked to a user, accessing the user
+ # subresource of the address path returns the user JSON representation.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ user_manager.create_user('anne@example.com', 'Anne')
+ json, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user')
+ self.assertEqual(headers['status'], '200')
+ self.assertEqual(json['user_id'], 1)
+ self.assertEqual(json['display_name'], 'Anne')
+ user_resource = json['self_link']
+ self.assertEqual(user_resource, 'http://localhost:9001/3.0/users/1')
+ # The self_link points to the correct user.
+ json, headers = call_api(user_resource)
+ self.assertEqual(json['user_id'], 1)
+ self.assertEqual(json['display_name'], 'Anne')
+ self.assertEqual(json['self_link'], user_resource)
+
+ def test_user_subresource_post(self):
+ # If the address is not yet linked to a user, POSTing a user id to the
+ # 'user' subresource links the address to the given user.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ anne = user_manager.create_user('anne.person@example.org', 'Anne')
+ anne_addr = user_manager.create_address('anne@example.com')
+ response, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+ 'user_id': anne.user_id.int,
+ })
+ self.assertEqual(headers['status'], '200')
+ self.assertEqual(anne_addr.user, anne)
+ self.assertEqual(sorted([a.email for a in anne.addresses]),
+ ['anne.person@example.org', 'anne@example.com'])
+
+ def test_user_subresource_post_new_user(self):
+ # If the address is not yet linked to a user, POSTing to the 'user'
+ # subresources creates a new user object and links it to the address.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ anne_addr = user_manager.create_address('anne@example.com')
+ response, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+ 'display_name': 'Anne',
+ })
+ self.assertEqual(headers['status'], '201')
+ anne = user_manager.get_user('anne@example.com')
+ self.assertIsNotNone(anne)
+ self.assertEqual(anne.display_name, 'Anne')
+ self.assertEqual([a.email for a in anne.addresses],
+ ['anne@example.com'])
+ self.assertEqual(anne_addr.user, anne)
+ self.assertEqual(headers['location'],
+ 'http://localhost:9001/3.0/users/1')
+
+ def test_user_subresource_post_conflict(self):
+ # If the address is already linked to a user, trying to link it to
+ # another user produces a 409 Conflict error.
+ with transaction():
+ getUtility(IUserManager).create_user('anne@example.com')
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+ 'email': 'anne.person@example.org',
+ })
+ self.assertEqual(cm.exception.code, 409)
+
+ def test_user_subresource_post_new_user_no_auto_create(self):
+ # By default, POSTing to the 'user' resource of an unlinked address
+ # will automatically create the user. By setting a boolean
+ # 'auto_create' flag to false, you can prevent this.
+ with transaction():
+ getUtility(IUserManager).create_address('anne@example.com')
+ with self.assertRaises(HTTPError) as cm:
+ json, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+ 'display_name': 'Anne',
+ 'auto_create': 0,
+ })
+ self.assertEqual(cm.exception.code, 403)
+
+ def test_user_subresource_unlink(self):
+ # By DELETEing the usr subresource, you can unlink a user from an
+ # address.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ user_manager.create_user('anne@example.com')
+ response, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user',
+ method='DELETE')
+ self.assertEqual(headers['status'], '204')
+ anne_addr = user_manager.get_address('anne@example.com')
+ self.assertIsNone(anne_addr.user, 'The address is still linked')
+ self.assertIsNone(user_manager.get_user('anne@example.com'))
+
+ def test_user_subresource_unlink_unlinked(self):
+ # If you try to unlink an unlinked address, you get a 404 error.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ user_manager.create_address('anne@example.com')
+ with self.assertRaises(HTTPError) as cm:
+ response, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user',
+ method='DELETE')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_user_subresource_put(self):
+ # By PUTing to the 'user' resource, you can change the user that an
+ # address is linked to.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ anne = user_manager.create_user('anne@example.com', 'Anne')
+ bart = user_manager.create_user(display_name='Bart')
+ response, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+ 'user_id': bart.user_id.int,
+ }, method='PUT')
+ self.assertEqual(headers['status'], '200')
+ self.assertEqual(anne.addresses, [])
+ self.assertEqual([address.email for address in bart.addresses],
+ ['anne@example.com'])
+ self.assertEqual(bart,
+ user_manager.get_address('anne@example.com').user)
+
+ def test_user_subresource_put_create(self):
+ # PUTing to the 'user' resource creates the user, just like with POST.
+ user_manager = getUtility(IUserManager)
+ with transaction():
+ anne = user_manager.create_user('anne@example.com', 'Anne')
+ response, headers = call_api(
+ 'http://localhost:9001/3.0/addresses/anne@example.com/user', {
+ 'email': 'anne.person@example.org',
+ }, method='PUT')
+ self.assertEqual(headers['status'], '201')
+ self.assertEqual(anne.addresses, [])
+ anne_person = user_manager.get_user('anne.person@example.org')
+ self.assertIsNotNone(anne_person)
+ self.assertEqual(
+ sorted([address.email for address in anne_person.addresses]),
+ ['anne.person@example.org', 'anne@example.com'])
+ anne_addr = user_manager.get_address('anne@example.com')
+ self.assertIsNotNone(anne_addr)
+ self.assertEqual(anne_addr.user, anne_person)
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index cfea36cfa..7ab1d6818 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -27,6 +27,7 @@ __all__ = [
]
+from lazr.config import as_boolean
from passlib.utils import generate_password as generate
from uuid import UUID
from zope.component import getUtility
@@ -39,7 +40,8 @@ from mailman.interfaces.usermanager import IUserManager
from mailman.rest.addresses import UserAddresses
from mailman.rest.helpers import (
BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child,
- created, etag, forbidden, no_content, not_found, okay, paginate, path_to)
+ conflict, created, etag, forbidden, no_content, not_found, okay, paginate,
+ path_to)
from mailman.rest.preferences import Preferences
from mailman.rest.validator import PatchValidator, Validator
@@ -63,6 +65,35 @@ ATTRIBUTES = dict(
)
+CREATION_FIELDS = dict(
+ email=unicode,
+ display_name=unicode,
+ password=unicode,
+ _optional=('display_name', 'password'),
+ )
+
+
+def create_user(arguments, response):
+ """Create a new user."""
+ # We can't pass the 'password' argument to the user creation method, so
+ # strip that out (if it exists), then create the user, adding the password
+ # after the fact if successful.
+ password = arguments.pop('password', None)
+ try:
+ user = getUtility(IUserManager).create_user(**arguments)
+ except ExistingAddressError as error:
+ bad_request(
+ response, b'Address already exists: {}'.format(error.address))
+ return None
+ if password is None:
+ # This will have to be reset since it cannot be retrieved.
+ password = generate(int(config.passwords.password_length))
+ user.password = config.password_context.encrypt(password)
+ location = path_to('users/{}'.format(user.user_id.int))
+ created(response, location)
+ return user
+
+
class _UserBase(CollectionMixin):
"""Shared base class for user representations."""
@@ -77,7 +108,7 @@ class _UserBase(CollectionMixin):
resource = dict(
user_id=user_id,
created_on=user.created_on,
- self_link=path_to('users/{0}'.format(user_id)),
+ self_link=path_to('users/{}'.format(user_id)),
)
# Add the password attribute, only if the user has a password. Same
# with the real name. These could be None or the empty string.
@@ -105,30 +136,12 @@ class AllUsers(_UserBase):
def on_post(self, request, response):
"""Create a new user."""
try:
- validator = Validator(email=unicode,
- display_name=unicode,
- password=unicode,
- _optional=('display_name', 'password'))
+ validator = Validator(**CREATION_FIELDS)
arguments = validator(request)
except ValueError as error:
bad_request(response, str(error))
return
- # We can't pass the 'password' argument to the user creation method,
- # so strip that out (if it exists), then create the user, adding the
- # password after the fact if successful.
- password = arguments.pop('password', None)
- try:
- user = getUtility(IUserManager).create_user(**arguments)
- except ExistingAddressError as error:
- bad_request(
- response, b'Address already exists: {0}'.format(error.address))
- return
- if password is None:
- # This will have to be reset since it cannot be retrieved.
- password = generate(int(config.passwords.password_length))
- user.password = config.password_context.encrypt(password)
- location = path_to('users/{0}'.format(user.user_id.int))
- created(response, location)
+ create_user(arguments, response)
@@ -242,6 +255,100 @@ class AUser(_UserBase):
+class AddressUser(_UserBase):
+ """The user linked to an address."""
+
+ def __init__(self, address):
+ self._address = address
+ self._user = address.user
+
+ def on_get(self, request, response):
+ """Return a single user end-point."""
+ if self._user is None:
+ not_found(response)
+ else:
+ okay(response, self._resource_as_json(self._user))
+
+ def on_delete(self, request, response):
+ """Delete the named user, all her memberships, and addresses."""
+ if self._user is None:
+ not_found(response)
+ return
+ self._user.unlink(self._address)
+ no_content(response)
+
+ def on_post(self, request, response):
+ """Link a user to the address, and create it if needed."""
+ if self._user:
+ conflict(response)
+ return
+ # When creating a linked user by POSTing, the user either must already
+ # exist, or it can be automatically created, if the auto_create flag
+ # is given and true (if missing, it defaults to true). However, in
+ # this case we do not accept 'email' as a POST field.
+ fields = CREATION_FIELDS.copy()
+ del fields['email']
+ fields['user_id'] = int
+ fields['auto_create'] = as_boolean
+ fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create')
+ try:
+ validator = Validator(**fields)
+ arguments = validator(request)
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ user_manager = getUtility(IUserManager)
+ if 'user_id' in arguments:
+ raw_uid = arguments['user_id']
+ user_id = UUID(int=raw_uid)
+ user = user_manager.get_user_by_id(user_id)
+ if user is None:
+ not_found(response, b'No user with ID {}'.format(raw_uid))
+ return
+ okay(response)
+ else:
+ auto_create = arguments.pop('auto_create', True)
+ if auto_create:
+ # This sets the 201 or 400 status.
+ user = create_user(arguments, response)
+ if user is None:
+ return
+ else:
+ forbidden(response)
+ return
+ user.link(self._address)
+
+ def on_put(self, request, response):
+ """Set or replace the addresses's user."""
+ if self._user:
+ self._user.unlink(self._address)
+ # Process post data and check for an existing user.
+ fields = CREATION_FIELDS.copy()
+ fields['user_id'] = int
+ fields['_optional'] = fields['_optional'] + ('user_id', 'email')
+ try:
+ validator = Validator(**fields)
+ arguments = validator(request)
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ user_manager = getUtility(IUserManager)
+ if 'user_id' in arguments:
+ raw_uid = arguments['user_id']
+ user_id = UUID(int=raw_uid)
+ user = user_manager.get_user_by_id(user_id)
+ if user is None:
+ not_found(response, b'No user with ID {}'.format(raw_uid))
+ return
+ okay(response)
+ else:
+ user = create_user(arguments, response)
+ if user is None:
+ return
+ user.link(self._address)
+
+
+
class Login:
"""<api>/users/<uid>/login"""
diff --git a/src/mailman/rules/approved.py b/src/mailman/rules/approved.py
index 2839ffef4..3b40d5dc9 100644
--- a/src/mailman/rules/approved.py
+++ b/src/mailman/rules/approved.py
@@ -71,9 +71,10 @@ class Approved:
# Find the first text/plain part in the message
part = None
stripped = False
+ payload = None
for part in typed_subpart_iterator(msg, 'text', 'plain'):
+ payload = part.get_payload(decode=True)
break
- payload = part.get_payload(decode=True)
if payload is not None:
charset = part.get_content_charset('us-ascii')
payload = payload.decode(charset, 'replace')
diff --git a/src/mailman/rules/implicit_dest.py b/src/mailman/rules/implicit_dest.py
index 28effc490..0bc229b15 100644
--- a/src/mailman/rules/implicit_dest.py
+++ b/src/mailman/rules/implicit_dest.py
@@ -74,7 +74,7 @@ class ImplicitDestination:
for header in ('to', 'cc', 'resent-to', 'resent-cc'):
for fullname, address in getaddresses(msg.get_all(header, [])):
if isinstance(address, bytes):
- address = address.decode("ascii")
+ address = address.decode('ascii')
address = address.lower()
if address in aliases:
return False
diff --git a/src/mailman/rules/tests/test_approved.py b/src/mailman/rules/tests/test_approved.py
index e7f122410..9976d4eff 100644
--- a/src/mailman/rules/tests/test_approved.py
+++ b/src/mailman/rules/tests/test_approved.py
@@ -491,3 +491,35 @@ deprecated = roundup_plaintext
self.assertFalse(result)
self.assertEqual(self._mlist.moderator_password,
b'{plaintext}super secret')
+
+
+class TestApprovedNoTextPlainPart(unittest.TestCase):
+ """Test the approved handler with HTML-only messages."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._rule = approved.Approved()
+
+ def test_no_text_plain_part(self):
+ # When the message body only contains HTML, the rule should not throw
+ # AttributeError: 'NoneType' object has no attribute 'get_payload'
+ # LP: #1158721
+ msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Subject: HTML only email
+Message-ID: <ant>
+MIME-Version: 1.0
+Content-Type: text/html; charset="Windows-1251"
+Content-Transfer-Encoding: 7bit
+
+<HTML>
+<BODY>
+<P>This message contains only HTML, no plain/text part</P>
+</BODY>
+</HTML>
+""")
+ result = self._rule.check(self._mlist, msg, {})
+ self.assertFalse(result)
diff --git a/src/mailman/utilities/email.py b/src/mailman/utilities/email.py
index ee23bf9e8..ea44ad0a4 100644
--- a/src/mailman/utilities/email.py
+++ b/src/mailman/utilities/email.py
@@ -63,7 +63,7 @@ def add_message_hash(msg):
if message_id is None:
return
if isinstance(message_id, bytes):
- message_id = message_id.decode("ascii")
+ message_id = message_id.decode('ascii')
# The angle brackets are not part of the Message-ID. See RFC 2822
# and http://wiki.list.org/display/DEV/Stable+URLs
if message_id.startswith('<') and message_id.endswith('>'):