Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

# -*- coding: utf-8 -*- 

# Copyright (C) 1998-2019 by the Free Software Foundation, Inc. 

# 

# This file is part of Postorius. 

# 

# Postorius is free software: you can redistribute it and/or modify it under 

# the terms of the GNU General Public License as published by the Free 

# Software Foundation, either version 3 of the License, or (at your option) 

# any later version. 

# 

# Postorius is distributed in the hope that it will be useful, but WITHOUT 

# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

# more details. 

# 

# You should have received a copy of the GNU General Public License along with 

# Postorius. If not, see <http://www.gnu.org/licenses/>. 

 

from __future__ import ( 

absolute_import, division, print_function, unicode_literals) 

 

import logging 

from urllib.parse import urljoin 

 

from django.conf import settings 

from django.core.exceptions import ImproperlyConfigured 

from django.contrib.auth.models import User 

from django.db import models 

from django.db.models.signals import post_save, post_delete 

from django.dispatch import receiver 

from django.http import Http404 

from django.urls import reverse 

from django.utils.six.moves.urllib.error import HTTPError 

from django.utils.translation import ugettext_lazy as _ 

from mailmanclient import MailmanConnectionError 

 

from postorius.utils import get_mailman_client, LANGUAGES 

from postorius.template_list import TEMPLATES_LIST 

 

logger = logging.getLogger(__name__) 

 

 

@receiver(post_save, sender=User) 

def create_mailman_user(sender, **kwargs): 

if kwargs.get('created'): 

if getattr(settings, 'AUTOCREATE_MAILMAN_USER', False): 

user = kwargs.get('instance') 

try: 

MailmanUser.objects.create_from_django(user) 

except (MailmanApiError, HTTPError): 

logger.error('Mailman user not created for {}'.format(user)) 

logger.error('Mailman Core API is not reachable.') 

 

 

class MailmanApiError(Exception): 

"""Raised if the API is not available. 

""" 

pass 

 

 

class Mailman404Error(Exception): 

"""Proxy exception. Raised if the API returns 404.""" 

pass 

 

 

class MailmanRestManager(object): 

"""Manager class to give a model class CRUD access to the API. 

Returns objects (or lists of objects) retrieved from the API. 

""" 

 

def __init__(self, resource_name, resource_name_plural, cls_name=None): 

self.resource_name = resource_name 

self.resource_name_plural = resource_name_plural 

 

def all(self): 

try: 

return getattr(get_mailman_client(), self.resource_name_plural) 

except AttributeError: 

raise MailmanApiError 

except MailmanConnectionError as e: 

raise MailmanApiError(e) 

 

def get(self, *args, **kwargs): 

try: 

method = getattr(get_mailman_client(), 'get_' + self.resource_name) 

return method(*args, **kwargs) 

except AttributeError as e: 

raise MailmanApiError(e) 

except HTTPError as e: 

if e.code == 404: 

raise Mailman404Error('Mailman resource could not be found.') 

else: 

raise 

except MailmanConnectionError as e: 

raise MailmanApiError(e) 

 

def get_or_404(self, *args, **kwargs): 

"""Similar to `self.get` but raises standard Django 404 error. 

""" 

try: 

return self.get(*args, **kwargs) 

except Mailman404Error: 

raise Http404 

except MailmanConnectionError as e: 

raise MailmanApiError(e) 

 

def create(self, *args, **kwargs): 

try: 

method = getattr( 

get_mailman_client(), 'create_' + self.resource_name) 

return method(*args, **kwargs) 

except AttributeError as e: 

raise MailmanApiError(e) 

except HTTPError as e: 

if e.code == 409: 

raise MailmanApiError 

else: 

raise 

except MailmanConnectionError: 

raise MailmanApiError 

 

def delete(self): 

"""Not implemented since the objects returned from the API 

have a `delete` method of their own. 

""" 

pass 

 

 

class MailmanListManager(MailmanRestManager): 

 

def __init__(self): 

super(MailmanListManager, self).__init__('list', 'lists') 

 

def all(self, advertised=False): 

try: 

method = getattr( 

get_mailman_client(), 'get_' + self.resource_name_plural) 

return method(advertised=advertised) 

except AttributeError: 

raise MailmanApiError 

except MailmanConnectionError as e: 

raise MailmanApiError(e) 

 

def by_mail_host(self, mail_host, advertised=False): 

objects = self.all(advertised) 

host_objects = [] 

for obj in objects: 

if obj.mail_host == mail_host: 

host_objects.append(obj) 

return host_objects 

 

 

class MailmanUserManager(MailmanRestManager): 

 

def __init__(self): 

super(MailmanUserManager, self).__init__('user', 'users') 

 

def create_from_django(self, user): 

return self.create( 

email=user.email, password=None, display_name=user.get_full_name()) 

 

def get_or_create_from_django(self, user): 

try: 

return self.get(address=user.email) 

except Mailman404Error: 

return self.create_from_django(user) 

 

 

class MailmanRestModel(object): 

"""Simple REST Model class to make REST API calls Django style. 

""" 

MailmanApiError = MailmanApiError 

DoesNotExist = Mailman404Error 

 

def __init__(self, *args, **kwargs): 

self.args = args 

self.kwargs = kwargs 

 

def save(self): 

"""Proxy function for `objects.create`. 

(REST API uses `create`, while Django uses `save`.) 

""" 

self.objects.create(*self.args, **self.kwargs) 

 

 

class Domain(MailmanRestModel): 

"""Domain model class. 

""" 

objects = MailmanRestManager('domain', 'domains') 

 

 

class List(MailmanRestModel): 

"""List model class. 

""" 

objects = MailmanListManager() 

 

 

class MailmanUser(MailmanRestModel): 

"""MailmanUser model class. 

""" 

objects = MailmanUserManager() 

 

 

class Member(MailmanRestModel): 

"""Member model class. 

""" 

objects = MailmanRestManager('member', 'members') 

 

 

class Style(MailmanRestModel): 

""" 

""" 

objects = MailmanRestManager(None, 'styles') 

 

 

TEMPLATE_CONTEXT_CHOICES = ( 

('site', 'Site Wide'), 

('domain', 'Domain Wide'), 

('list', 'MailingList Wide') 

) 

 

 

class EmailTemplate(models.Model): 

"""A Template represents contents of partial or complete emails sent out by 

Mailman Core on various events or when an action is required. Headers and 

Footers on emails for decorations are also repsented as templates. 

""" 

 

name = models.CharField( 

max_length=100, choices=TEMPLATES_LIST, 

help_text=_('Choose the template you want to customize.')) 

data = models.TextField( 

help_text=_( 

'Note: Do not add any secret content in templates as they are ' 

'publicly accessible.\n' 

'You can use these variables in the templates. \n' 

'$hyperkitty_url: Permalink to archived message in Hyperkitty\n' 

'$listname: Name of the Mailing List e.g. ant@example.com \n' 

'$list_id: The List-ID header e.g. ant.example.com \n' 

'$display_name: Display name of the mailing list e.g. Ant \n' 

'$short_listname: Local part of the listname e.g. ant \n' 

'$domain: The domain part of the listname e.g. example.com \n' 

'$info: The mailing list\'s longer descriptive text \n' 

'$request_email: The email address for -request address \n' 

'$owner_email: The email address for -owner address \n' 

'$site_email: The email address to reach the owners of the site \n' 

'$language: The two letter language code for list\'s preferred language e.g. fr, en, de \n' # noqa: E501 

) 

) 

language = models.CharField( 

max_length=5, choices=LANGUAGES, 

help_text=_('Language for the template, this should be the list\'s preferred language.'), # noqa: E501 

blank=True) 

craeted_at = models.DateTimeField(auto_now_add=True) 

modified_at = models.DateTimeField(auto_now=True) 

context = models.CharField(max_length=50, choices=TEMPLATE_CONTEXT_CHOICES) 

identifier = models.CharField(blank=True, max_length=100) 

 

class Meta: 

unique_together = ('name', 'identifier', 'language') 

 

def __str__(self): 

return '<EmailTemplate {0} for {1}>'.format(self.name, self.context) 

 

@property 

def description(self): 

"""Return the long description of template that is human readable.""" 

return dict(TEMPLATES_LIST)[self.name] 

 

@property 

def api_url(self): 

"""API url is the remote url that Core can use to fetch templates""" 

base_url = getattr(settings, 'POSTORIUS_TEMPLATE_BASE_URL', None) 

if not base_url: 

raise ImproperlyConfigured 

resource_url = reverse( 

'rest_template', 

kwargs=dict(context=self.context, 

identifier=self.identifier, 

name=self.name) 

) 

return urljoin(base_url, resource_url) 

 

def _get_context_obj(self): 

if self.context == 'list': 

obj = List.objects.get_or_404(fqdn_listname=self.identifier) 

elif self.context == 'domain': 

obj = Domain.objects.get_or_404(mail_host=self.identifier) 

elif self.context == 'site': 

obj = get_mailman_client() 

else: 

obj = None 

return obj 

 

def _update_core(self, deleted=False): 

obj = self._get_context_obj() 

if obj is None: 

return 

 

if deleted: 

# POST'ing an empty string will delete this record in Core. 

api_url = '' 

else: 

# Use the API endpoint of self that Core can use to fetch this. 

api_url = self.api_url 

obj.set_template(self.name, api_url) 

 

 

@receiver(post_save, sender=EmailTemplate) 

def update_core_post_update(sender, **kwargs): 

kwargs['instance']._update_core() 

 

 

@receiver(post_delete, sender=EmailTemplate) 

def update_core_post_delete(sender, **kwargs): 

kwargs['instance']._update_core(deleted=True)