Coverage for distro_tracker/mail/control/commands/misc.py: 97%
151 statements
« prev ^ index » next coverage.py v6.5.0, created at 2025-01-12 09:15 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2025-01-12 09:15 +0000
1# Copyright 2013-2015 The Distro Tracker Developers
2# See the COPYRIGHT file at the top-level directory of this distribution and
3# at https://deb.li/DTAuthors
4#
5# This file is part of Distro Tracker. It is subject to the license terms
6# in the LICENSE file found in the top-level directory of this
7# distribution and at https://deb.li/DTLicense. No part of Distro Tracker,
8# including this file, may be copied, modified, propagated, or distributed
9# except according to the terms contained in the LICENSE file.
10"""
11Implementation of miscellaneous commands.
12"""
14from django.conf import settings
15from django.core.exceptions import ValidationError
17from distro_tracker.core.models import (
18 BinaryPackageName,
19 EmailSettings,
20 PackageName,
21 PseudoPackageName,
22 SourcePackageName,
23 Subscription,
24 UserEmail
25)
26from distro_tracker.core.utils import (
27 distro_tracker_render_to_string,
28 get_or_none
29)
30from distro_tracker.mail.control.commands.base import Command
31from distro_tracker.mail.control.commands.confirmation import needs_confirmation
33DISTRO_TRACKER_FQDN = settings.DISTRO_TRACKER_FQDN
36@needs_confirmation
37class SubscribeCommand(Command):
38 """
39 A command which subscribes a user to a package so that they
40 receive that package's email messages.
42 .. note::
43 This command requires confirmation.
44 """
45 META = {
46 'description': """subscribe <srcpackage> [<email>]
47 Subscribes <email> to all messages regarding <srcpackage>. If
48 <email> is not given, it subscribes the From address. If the
49 <srcpackage> is not a valid source package, you'll get a warning.
50 If it's a valid binary package, the mapping will automatically be
51 done for you.""",
52 'name': 'subscribe',
53 'position': 1,
54 }
56 REGEX_LIST = (
57 r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+))?$',
58 )
60 def __init__(self, package, email):
61 super(SubscribeCommand, self).__init__()
62 self.package = package
63 self.user_email = email
65 def get_command_text(self):
66 return super(SubscribeCommand, self).get_command_text(
67 self.package, self.user_email)
69 def pre_confirm(self):
70 """
71 Implementation of a hook method which is executed instead of
72 :py:meth:`handle` when the command is not confirmed.
73 """
75 if not self.validate_email(self.user_email):
76 self.warning('%s is not a valid email', self.user_email)
77 return False
79 settings = get_or_none(EmailSettings,
80 user_email__email__iexact=self.user_email)
81 if settings and settings.is_subscribed_to(self.package):
82 self.warning('%s is already subscribed to %s',
83 self.user_email, self.package)
84 return False
86 if not SourcePackageName.objects.exists_with_name(self.package):
87 if BinaryPackageName.objects.exists_with_name(self.package):
88 binary_package = \
89 BinaryPackageName.objects.get_by_name(self.package)
90 self.warning('%s is not a source package.', self.package)
91 self.reply('%s is the source package '
92 'for the %s binary package',
93 binary_package.main_source_package_name,
94 binary_package.name)
95 self.package = binary_package.main_source_package_name.name
96 else:
97 self.warning('%s is neither a source package '
98 'nor a binary package.', self.package)
99 if PseudoPackageName.objects.exists_with_name(self.package):
100 self.warning('Package %s is a pseudo package.',
101 self.package)
102 else:
103 self.warning('Package %s is not even a pseudo package.',
104 self.package)
106 try:
107 Subscription.objects.create_for(
108 email=self.user_email,
109 package_name=self.package,
110 active=False)
111 except ValidationError as e:
112 self.warning('%s', e.message)
113 return False
115 self.reply('A confirmation mail has been sent to %s', self.user_email)
116 return True
118 def handle(self):
119 subscription = Subscription.objects.create_for(
120 package_name=self.package,
121 email=self.user_email,
122 active=True)
123 if subscription: 123 ↛ 127line 123 didn't jump to line 127, because the condition on line 123 was never false
124 self.reply('%s has been subscribed to %s', self.user_email,
125 self.package)
126 else:
127 self.error('Could not subscribe %s to %s', self.user_email,
128 self.package)
130 def get_confirmation_message(self):
131 """
132 :returns: A message giving additional information about subscribing to
133 a package.
134 :rtype: string
135 """
136 return distro_tracker_render_to_string(
137 'control/email-subscription-confirmation.txt', {
138 'package': self.package,
139 }
140 )
143@needs_confirmation
144class UnsubscribeCommand(Command):
145 """
146 Command which unsubscribes the user from a package so that they no
147 longer receive any email messages regarding this package.
149 .. note::
150 This command requires confirmation.
151 """
152 META = {
153 'description': """unsubscribe <srcpackage> [<email>]
154 Unsubscribes <email> from <srcpackage>. Like the subscribe command,
155 it will use the From address if <email> is not given.""",
156 'name': 'unsubscribe',
157 'position': 2,
158 }
160 REGEX_LIST = (
161 r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+))?$',
162 )
164 def __init__(self, package, email):
165 super(UnsubscribeCommand, self).__init__()
166 self.package = package
167 self.user_email = email
169 def get_command_text(self):
170 return super(UnsubscribeCommand, self).get_command_text(
171 self.package, self.user_email)
173 def pre_confirm(self):
174 """
175 Implementation of a hook method which is executed instead of
176 :py:meth:`handle` when the command is not confirmed.
177 """
178 if not SourcePackageName.objects.exists_with_name(self.package):
179 if BinaryPackageName.objects.exists_with_name(self.package):
180 binary_package = \
181 BinaryPackageName.objects.get_by_name(self.package)
182 self.warning('%s is not a source package.', self.package)
183 self.reply('%s is the source package '
184 'for the %s binary package',
185 binary_package.main_source_package_name,
186 binary_package.name)
187 self.package = binary_package.main_source_package_name.name
188 else:
189 self.warning('%s is neither a source package '
190 'nor a binary package.', self.package)
191 settings = get_or_none(EmailSettings,
192 user_email__email__iexact=self.user_email)
193 if not settings or not settings.is_subscribed_to(self.package):
194 self.error("%s is not subscribed, you can't unsubscribe.",
195 self.user_email)
196 return False
198 self.reply('A confirmation mail has been sent to %s', self.user_email)
199 return True
201 def handle(self):
202 success = Subscription.objects.unsubscribe(self.package,
203 self.user_email)
204 if success: 204 ↛ 208line 204 didn't jump to line 208, because the condition on line 204 was never false
205 self.reply('%s has been unsubscribed from %s', self.user_email,
206 self.package)
207 else:
208 self.error('Could not unsubscribe %s from %s', self.user_email,
209 self.package)
211 def get_confirmation_message(self):
212 """
213 :returns: A message giving additional information about unsubscribing
214 from a package.
215 :rtype: string
216 """
217 return distro_tracker_render_to_string(
218 'control/email-unsubscribe-confirmation.txt', {
219 'package': self.package,
220 }
221 )
224class WhichCommand(Command):
225 """
226 A command which returns a list of packages to which the given user is
227 subscribed to.
228 """
229 META = {
230 'description': """which [<email>]
231 Tells you which packages <email> is subscribed to.""",
232 'name': 'which',
233 'position': 4,
234 }
236 REGEX_LIST = (
237 r'(?:\s+(?P<email>\S+))?$',
238 )
240 def __init__(self, email):
241 super(WhichCommand, self).__init__()
242 self.user_email = email
244 def get_command_text(self):
245 return super(WhichCommand, self).get_command_text(self.user_email)
247 def handle(self):
248 user_subscriptions = Subscription.objects.get_for_email(
249 self.user_email)
250 if not user_subscriptions:
251 self.reply('No subscriptions found')
252 return
253 self.list_reply(sub.package for sub in user_subscriptions)
256class WhoCommand(Command):
257 """
258 A command which returns a list of users which are subscribed to the given
259 package.
260 """
261 META = {
262 'description': """who <package>
263 Outputs all the subscriber emails for the given package in
264 an obfuscated form.""",
265 'name': 'who',
266 'position': 5,
267 }
269 REGEX_LIST = (
270 r'(?:\s+(?P<package>\S+))$',
271 )
273 def __init__(self, package):
274 super(WhoCommand, self).__init__()
275 self.package_name = package
277 def get_command_text(self):
278 return super(WhoCommand, self).get_command_text(self.package_name)
280 def handle(self):
281 package = get_or_none(PackageName, name=self.package_name)
282 if not package:
283 self.error('Package %s does not exist', self.package_name)
284 return
286 if package.subscriptions.count() == 0:
287 self.reply('Package %s does not have any subscribers', package.name)
288 return
290 self.reply("Here's the list of subscribers to package %s:",
291 self.package_name)
292 self.list_reply(
293 self.obfuscate(subscriber)
294 for subscriber in package.subscriptions.all()
295 )
297 def obfuscate(self, user_email):
298 """
299 Helper method which obfuscates the given email.
301 :param user_email: The user whose email should be obfuscated.
302 :type user_email:
303 :py:class:`UserEmail <distro_tracker.core.models.UserEmail>`
305 :returns: An obfuscated email address of the given user.
306 :rtype: string
307 """
308 email = user_email.email
309 local_part, domain = email.rsplit('@', 1)
310 domain_parts = domain.split('.')
311 obfuscated_domain = '.'.join(
312 part[0] + '.' * (len(part) - 1)
313 for part in domain_parts
314 )
315 return local_part + '@' + obfuscated_domain
318class QuitCommand(Command):
319 """
320 When this command is executed, the processing of further commands should
321 stop.
322 """
323 META = {
324 'description': '''quit
325 Stops processing commands''',
326 'name': 'quit',
327 'aliases': ['thanks', '--'],
328 'position': 6
329 }
331 REGEX_LIST = (
332 r'$',
333 )
335 def handle(self):
336 self.reply('Stopping processing here.')
339@needs_confirmation
340class UnsubscribeallCommand(Command):
341 """
342 Command which unsubscribes the user from all packages so that they
343 no longer receive any email messages regarding any packages.
345 .. note::
346 This command requires confirmation.
347 """
348 META = {
349 'description': '''unsubscribeall [<email>]
350 Cancel all subscriptions of <email>. Like the subscribe command,
351 it will use the From address if <email> is not given.''',
352 'name': 'unsubscribeall',
353 'position': 7,
354 }
356 REGEX_LIST = (
357 r'(?:\s+(?P<email>\S+))?$',
358 )
360 def __init__(self, email):
361 super(UnsubscribeallCommand, self).__init__()
362 self.user_email = email
364 def get_command_text(self):
365 return super(UnsubscribeallCommand, self).get_command_text(
366 self.user_email)
368 def pre_confirm(self):
369 """
370 Implementation of a hook method which is executed instead of
371 :py:meth:`handle` when the command is not confirmed.
372 """
373 settings = get_or_none(EmailSettings,
374 user_email__email__iexact=self.user_email)
375 if not settings or settings.subscription_set.count() == 0:
376 self.warning('User %s is not subscribed to any packages',
377 self.user_email)
378 return False
380 self.reply('A confirmation mail has been sent to %s', self.user_email)
381 return True
383 def handle(self):
384 user = get_or_none(UserEmail, email__iexact=self.user_email)
385 email_settings = get_or_none(EmailSettings, user_email=user)
386 if user is None or email_settings is None: 386 ↛ 387line 386 didn't jump to line 387, because the condition on line 386 was never true
387 return
388 packages = [
389 subscription.package.name
390 for subscription in email_settings.subscription_set.all()
391 ]
392 email_settings.unsubscribe_all()
393 self.reply('All your subscriptions have been terminated:')
394 self.list_reply(
395 '{email} has been unsubscribed from {package}@{fqdn}'.format(
396 email=self.user_email,
397 package=package,
398 fqdn=DISTRO_TRACKER_FQDN)
399 for package in sorted(packages))
401 def get_confirmation_message(self):
402 """
403 :returns: A message giving additional information about unsubscribing
404 from all packages.
405 :rtype: string
406 """
407 return distro_tracker_render_to_string(
408 'control/email-unsubscribeall-confirmation.txt'
409 )