Merge branch 'alias' into 'master'
[mailman.git] / src / mailman / model / subscriptions.py
blobd803a08aa4394ffda9ca80a768c64867a010342b
1 # Copyright (C) 2016-2019 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Subscription services."""
20 from mailman.app.membership import delete_member
21 from mailman.database.transaction import dbconnection
22 from mailman.interfaces.listmanager import IListManager, NoSuchListError
23 from mailman.interfaces.member import MemberRole
24 from mailman.interfaces.subscriptions import (
25 ISubscriptionService, TooManyMembersError)
26 from mailman.interfaces.usermanager import IUserManager
27 from mailman.model.address import Address
28 from mailman.model.member import Member
29 from mailman.model.user import User
30 from mailman.utilities.queries import QuerySequence
31 from operator import attrgetter
32 from public import public
33 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
34 from zope.component import getUtility
35 from zope.interface import implementer
38 @public
39 @implementer(ISubscriptionService)
40 class SubscriptionService:
41 """Subscription services for the REST API."""
43 __name__ = 'members'
45 def get_members(self):
46 """See `ISubscriptionService`."""
47 # {list_id -> {role -> [members]}}
48 by_list = {}
49 user_manager = getUtility(IUserManager)
50 for member in user_manager.members:
51 by_role = by_list.setdefault(member.list_id, {})
52 members = by_role.setdefault(member.role.name, [])
53 members.append(member)
54 # Flatten into single list sorted as per the interface.
55 all_members = []
56 address_of_member = attrgetter('address.email')
57 for list_id in sorted(by_list):
58 by_role = by_list[list_id]
59 all_members.extend(
60 sorted(by_role.get('owner', []), key=address_of_member))
61 all_members.extend(
62 sorted(by_role.get('moderator', []), key=address_of_member))
63 all_members.extend(
64 sorted(by_role.get('member', []), key=address_of_member))
65 return all_members
67 @dbconnection
68 def get_member(self, store, member_id):
69 """See `ISubscriptionService`."""
70 members = store.query(Member).filter(Member._member_id == member_id)
71 if members.count() == 0:
72 return None
73 else:
74 assert members.count() == 1, 'Too many matching members'
75 return members[0]
77 @dbconnection
78 def _find_members(self, store, subscriber, list_id, role):
79 # If `subscriber` is a user id, then we'll search for all addresses
80 # which are controlled by the user, otherwise we'll just search for
81 # the given address.
82 if subscriber is None and list_id is None and role is None:
83 return None
84 order = (Member.list_id, Address.email, Member.role)
85 # Querying for the subscriber is the most complicated part, because
86 # the parameter can either be an email address or a user id. Start by
87 # building two queries, one joined on the member's address, and one
88 # joined on the member's user. Add the resulting email address to the
89 # selected values to be able to sort on it later on.
90 q_address = store.query(Member, Address.email).join(Member._address)
91 q_user = store.query(Member, Address.email).join(
92 User, User.id == Member.user_id).join(User._preferred_address)
93 if subscriber is not None:
94 if isinstance(subscriber, str):
95 # subscriber is an email address.
96 subscriber = subscriber.lower()
97 if '*' in subscriber:
98 subscriber = subscriber.replace('*', '%')
99 q_address = q_address.filter(
100 Address.email.like(subscriber))
101 q_user = q_user.filter(Address.email.like(subscriber))
102 else:
103 q_address = q_address.filter(Address.email == subscriber)
104 q_user = q_user.filter(Address.email == subscriber)
105 else:
106 # subscriber is a user id.
107 q_address = q_address.join(Address.user).filter(
108 User._user_id == subscriber)
109 q_user = q_user.filter(User._user_id == subscriber)
110 else:
111 # We're not searching for a subscriber so only select preferred
112 # addresses (see GL issue 227).
113 q_user = q_user.filter(Address.id == User._preferred_address_id)
114 # Add additional filters to both queries.
115 if list_id is not None:
116 q_address = q_address.filter(Member.list_id == list_id)
117 q_user = q_user.filter(Member.list_id == list_id)
118 if role is not None:
119 q_address = q_address.filter(Member.role == role)
120 q_user = q_user.filter(Member.role == role)
121 # Do a UNION of the two queries, sort the result and generate Members.
122 return q_address.union(q_user).order_by(*order).from_self(Member)
124 def find_members(self, subscriber=None, list_id=None, role=None):
125 """See `ISubscriptionService`."""
126 return QuerySequence(self._find_members(subscriber, list_id, role))
128 def find_member(self, subscriber=None, list_id=None, role=None):
129 """See `ISubscriptionService`."""
130 try:
131 result = self._find_members(subscriber, list_id, role)
132 return (result if result is None else result.one())
133 except NoResultFound:
134 return None
135 except MultipleResultsFound:
136 # Coerce the exception into a Mailman-layer exception so call
137 # sites don't have to import from SQLAlchemy, resulting in a layer
138 # violation.
139 raise TooManyMembersError(subscriber, list_id, role)
141 def __iter__(self):
142 yield from self.get_members()
144 def leave(self, list_id, email):
145 """See `ISubscriptionService`."""
146 mlist = getUtility(IListManager).get_by_list_id(list_id)
147 if mlist is None:
148 raise NoSuchListError(list_id)
149 # XXX for now, no notification or user acknowledgment.
150 delete_member(mlist, email, False, False)
152 @dbconnection
153 def unsubscribe_members(self, store, list_id, emails):
154 """See 'ISubscriptionService'."""
155 success = set()
156 fail = set()
157 mlist = getUtility(IListManager).get_by_list_id(list_id)
158 if mlist is None:
159 raise NoSuchListError(list_id)
160 # Start with a query on the matching list-id and role.
161 q_member = store.query(Member).filter(
162 Member.list_id == list_id,
163 Member.role == MemberRole.member)
164 # De-duplicate.
165 for email in set(emails):
166 unsubscribed = False
167 # Join with a queries matching the email address and preferred
168 # address of any subscribed user.
169 q_address = q_member.join(Member._address).filter(
170 Address.email == email)
171 q_user = q_member.join(Member._user).join(
172 User._preferred_address).filter(Address.email == email)
173 members = q_address.union(q_user).all()
174 for member in members:
175 member.unsubscribe()
176 unsubscribed = True
177 (success if unsubscribed else fail).add(email)
178 return success, fail