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""" 

13 

14from django.conf import settings 

15from django.core.exceptions import ValidationError 

16 

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 

32 

33DISTRO_TRACKER_FQDN = settings.DISTRO_TRACKER_FQDN 

34 

35 

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. 

41 

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 } 

55 

56 REGEX_LIST = ( 

57 r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+))?$', 

58 ) 

59 

60 def __init__(self, package, email): 

61 super(SubscribeCommand, self).__init__() 

62 self.package = package 

63 self.user_email = email 

64 

65 def get_command_text(self): 

66 return super(SubscribeCommand, self).get_command_text( 

67 self.package, self.user_email) 

68 

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 """ 

74 

75 if not self.validate_email(self.user_email): 

76 self.warning('%s is not a valid email', self.user_email) 

77 return False 

78 

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 

85 

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) 

105 

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 

114 

115 self.reply('A confirmation mail has been sent to %s', self.user_email) 

116 return True 

117 

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) 

129 

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 ) 

141 

142 

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. 

148 

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 } 

159 

160 REGEX_LIST = ( 

161 r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+))?$', 

162 ) 

163 

164 def __init__(self, package, email): 

165 super(UnsubscribeCommand, self).__init__() 

166 self.package = package 

167 self.user_email = email 

168 

169 def get_command_text(self): 

170 return super(UnsubscribeCommand, self).get_command_text( 

171 self.package, self.user_email) 

172 

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 

197 

198 self.reply('A confirmation mail has been sent to %s', self.user_email) 

199 return True 

200 

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) 

210 

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 ) 

222 

223 

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 } 

235 

236 REGEX_LIST = ( 

237 r'(?:\s+(?P<email>\S+))?$', 

238 ) 

239 

240 def __init__(self, email): 

241 super(WhichCommand, self).__init__() 

242 self.user_email = email 

243 

244 def get_command_text(self): 

245 return super(WhichCommand, self).get_command_text(self.user_email) 

246 

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) 

254 

255 

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 } 

268 

269 REGEX_LIST = ( 

270 r'(?:\s+(?P<package>\S+))$', 

271 ) 

272 

273 def __init__(self, package): 

274 super(WhoCommand, self).__init__() 

275 self.package_name = package 

276 

277 def get_command_text(self): 

278 return super(WhoCommand, self).get_command_text(self.package_name) 

279 

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 

285 

286 if package.subscriptions.count() == 0: 

287 self.reply('Package %s does not have any subscribers', package.name) 

288 return 

289 

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 ) 

296 

297 def obfuscate(self, user_email): 

298 """ 

299 Helper method which obfuscates the given email. 

300 

301 :param user_email: The user whose email should be obfuscated. 

302 :type user_email: 

303 :py:class:`UserEmail <distro_tracker.core.models.UserEmail>` 

304 

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 

316 

317 

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 } 

330 

331 REGEX_LIST = ( 

332 r'$', 

333 ) 

334 

335 def handle(self): 

336 self.reply('Stopping processing here.') 

337 

338 

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. 

344 

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 } 

355 

356 REGEX_LIST = ( 

357 r'(?:\s+(?P<email>\S+))?$', 

358 ) 

359 

360 def __init__(self, email): 

361 super(UnsubscribeallCommand, self).__init__() 

362 self.user_email = email 

363 

364 def get_command_text(self): 

365 return super(UnsubscribeallCommand, self).get_command_text( 

366 self.user_email) 

367 

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 

379 

380 self.reply('A confirmation mail has been sent to %s', self.user_email) 

381 return True 

382 

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)) 

400 

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 )