1# Copyright (C) 2007-2020 by the Free Software Foundation, Inc.
2#
3# This file is part of GNU Mailman.
4#
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.
9#
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.
14#
15# You should have received a copy of the GNU General Public License along with
16# GNU Mailman.  If not, see <https://www.gnu.org/licenses/>.
17
18"""Interface describing the basics of a member."""
19
20from enum import Enum
21from mailman.interfaces.errors import MailmanError
22from public import public
23from zope.interface import Attribute, Interface
24
25
26@public
27class DeliveryMode(Enum):
28    # Regular (i.e. non-digest) delivery
29    regular = 1
30    # Plain text digest delivery
31    plaintext_digests = 2
32    # MIME digest delivery
33    mime_digests = 3
34    # Summary digests
35    summary_digests = 4
36
37
38@public
39class DeliveryStatus(Enum):
40    # Delivery is enabled
41    enabled = 1
42    # Delivery was disabled by the user
43    by_user = 2
44    # Delivery was disabled due to bouncing addresses
45    by_bounces = 3
46    # Delivery was disabled by an administrator or moderator
47    by_moderator = 4
48    # Disabled for unknown reasons.
49    unknown = 5
50
51
52@public
53class MemberRole(Enum):
54    member = 1
55    owner = 2
56    moderator = 3
57    nonmember = 4
58
59
60@public
61class MembershipChangeEvent:
62    """Base class for subscription/unsubscription events."""
63
64    def __init__(self, mlist, member):
65        self.mlist = mlist
66        self.member = member
67
68
69@public
70class SubscriptionEvent(MembershipChangeEvent):
71    """Event which gets triggered when a user joins a mailing list."""
72
73    def __init__(self, *args, **kw):
74        send_welcome_message = kw.pop('send_welcome_message', None)
75        super().__init__(*args, **kw)
76        self.send_welcome_message = send_welcome_message
77
78    def __str__(self):
79        return '{0} joined {1}'.format(self.member.address, self.mlist.list_id)
80
81
82@public
83class UnsubscriptionEvent(MembershipChangeEvent):
84    """Event which gets triggered when a user leaves a mailing list.
85
86    One thing to keep in mind: because the IMember is deleted when the
87    unsubscription happens, this event actually gets triggered just before the
88    member is unsubscribed.
89    """
90
91    def __str__(self):
92        return '{0} left {1}'.format(self.member.address, self.mlist.list_id)
93
94
95@public
96class MembershipError(MailmanError):
97    """Base exception for all membership errors."""
98
99
100@public
101class AlreadySubscribedError(MembershipError):
102    """The member is already subscribed to the mailing list with this role."""
103
104    def __init__(self, fqdn_listname, email, role):
105        super().__init__()
106        self.fqdn_listname = fqdn_listname
107        self.email = email
108        self.role = role
109
110    def __str__(self):
111        return '{0} is already a {1} of mailing list {2}'.format(
112            self.email, self.role, self.fqdn_listname)
113
114
115@public
116class MembershipIsBannedError(MembershipError):
117    """The address is not allowed to subscribe to the mailing list."""
118
119    def __init__(self, mlist, address):
120        super().__init__()
121        self._mlist = mlist
122        self._address = address
123
124    def __str__(self):
125        return '{0} is not allowed to subscribe to {1.fqdn_listname}'.format(
126            self._address, self._mlist)
127
128
129@public
130class MissingPreferredAddressError(MembershipError):
131    """A user without a preferred address attempted to subscribe."""
132
133    def __init__(self, user):
134        super().__init__()
135        self._user = user
136
137    def __str__(self):
138        return 'User must have a preferred address: {0}'.format(self._user)
139
140
141@public
142class NotAMemberError(MembershipError):
143    """The address is not a member of the mailing list."""
144
145    def __init__(self, mlist, address):
146        super().__init__()
147        self._mlist = mlist
148        self._address = address
149
150    def __str__(self):
151        return '{0} is not a member of {1.fqdn_listname}'.format(
152            self._address, self._mlist)
153
154
155@public
156class IMember(Interface):
157    """A member of a mailing list."""
158
159    member_id = Attribute(
160        """The member's unique, random identifier as a UUID.""")
161
162    list_id = Attribute(
163        """The list id of the mailing list the member is subscribed to.""")
164
165    mailing_list = Attribute(
166        """The `IMailingList` that the member is subscribed to.""")
167
168    address = Attribute(
169        """The email address that's subscribed to the list.""")
170
171    user = Attribute(
172        """The user associated with this member.""")
173
174    subscriber = Attribute(
175        """The object representing how this member is subscribed.
176
177        This will be an ``IAddress`` if the user is subscribed via an explicit
178        address, otherwise if the the user is subscribed via their preferred
179        address, it will be an ``IUser``.
180        """)
181
182    display_name = Attribute(
183        """The best match of the member's display name.
184
185        This will be `subscriber.display_name` if available, which means it
186        will either be the display name of the address or user that's
187        subscribed.  If unavailable, and the address is the subscriber, then
188        the linked user's display name is given, if available.  When all else
189        fails, the empty string is returned.
190        """)
191
192    preferences = Attribute(
193        """This member's preferences.""")
194
195    role = Attribute(
196        """The role of this membership.""")
197
198    moderation_action = Attribute(
199        """The moderation action for this member as an `Action`.""")
200
201    def unsubscribe():
202        """Unsubscribe (and delete) this member from the mailing list."""
203
204    acknowledge_posts = Attribute(
205        """Send an acknowledgment for every posting?
206
207        Unlike going through the preferences, this attribute return the
208        preference value based on the following lookup order:
209
210        1. The member
211        2. The address
212        3. The user
213        4. System default
214        """)
215
216    preferred_language = Attribute(
217        """The preferred language for interacting with a mailing list.
218
219        Unlike going through the preferences, this attribute return the
220        preference value based on the following lookup order:
221
222        1. The member
223        2. The address
224        3. The user
225        4. System default
226        """)
227
228    receive_list_copy = Attribute(
229        """Should an explicit recipient receive a list copy?
230
231        Unlike going through `preferences`, this attribute returns the
232        preference value based on the following lookup order:
233
234        1. The member
235        2. The address
236        3. The user
237        4. System default
238        """)
239
240    receive_own_postings = Attribute(
241        """Should the poster get a list copy of their own messages?
242
243        Unlike going through `preferences`, this attribute returns the
244        preference value based on the following lookup order:
245
246        1. The member
247        2. The address
248        3. The user
249        4. System default
250        """)
251
252    delivery_mode = Attribute(
253        """The preferred delivery mode.
254
255        Unlike going through `preferences`, this attribute returns the
256        preference value based on the following lookup order:
257
258        1. The member
259        2. The address
260        3. The user
261        4. System default
262        """)
263
264    delivery_status = Attribute(
265        """The delivery status.
266
267        Unlike going through `preferences`, this attribute returns the
268        preference value based on the following lookup order:
269
270        1. The member
271        2. The address
272        3. The user
273        4. System default
274
275        XXX I'm not sure this is the right place to put this.""")
276
277    bounce_score = Attribute(
278        """The bounce score of this address for the list_id.
279
280        This is a metric of how much the emails sent to this address bounces.
281        This value is incremented first time a bounce is received for the
282        address and mailing list pair on that day.
283
284        The bounce_score of an address on one mailing list does not affect
285        their bounce_score on other mailing lists.
286        """)
287
288    last_bounce_received = Attribute(
289        """The last time when a bounce was received for this mailing list
290        address pair.
291        """)
292
293    last_warnings_sent = Attribute(
294        """The last time when a warning email was sent to the address""")
295
296    total_warnings_sent = Attribute(
297        """Total number of warnings sent to the address. """)
298
299    disabled = Attribute(
300        """Has the email delivery been disabled. """)
301
302    def reset_bounce_info():
303        """Reset the bounce related information. """
304
305
306@public
307class IMembershipManager(Interface):
308    """Member object manager."""
309
310    def memberships_pending_warning():
311        """Memberships that have been disabled due to excessive bounces and require a
312        warning to be sent.
313
314        The total number of warnings sent to these are less than the
315        MailingList's configured number of warnings to be sent before the
316        membership is removed.
317        """
318
319    def memberships_pending_removal():
320        """Memberships pending removal.
321
322        These memberships have maximum number of warnings already sent out and
323        are pending removal.
324        """
325