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
|
# Copyright (C) 2016 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman 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.
#
# GNU Mailman 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
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
"""Template management."""
import logging
from mailman import public
from mailman.config import config
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import SAUnicode
from mailman.interfaces.cache import ICacheManager
from mailman.interfaces.domain import IDomain
from mailman.interfaces.mailinglist import IMailingList
from mailman.interfaces.template import (
ALL_TEMPLATES, ITemplateLoader, ITemplateManager)
from mailman.utilities import protocols
from mailman.utilities.i18n import find
from mailman.utilities.string import expand
from requests import HTTPError
from sqlalchemy import Column, Integer
from urllib.error import URLError
from urllib.parse import urlparse
from zope.component import getUtility
from zope.interface import implementer
COMMASPACE = ', '
log = logging.getLogger('mailman.http')
class Template(Model):
__tablename__ = 'template'
id = Column(Integer, primary_key=True)
name = Column(SAUnicode)
context = Column(SAUnicode)
uri = Column(SAUnicode)
username = Column(SAUnicode, nullable=True)
password = Column(SAUnicode, nullable=True)
def __init__(self, name, context, uri, username, password):
self.name = name
self.context = context
self.reset(uri, username, password)
def reset(self, uri, username, password):
self.uri = uri
self.username = username
self.password = password
@public
@implementer(ITemplateManager)
class TemplateManager:
"""Manager of templates, with caching and support for mailman:// URIs."""
@dbconnection
def set(self, store, name, context, uri, username=None, password=''):
"""See `ITemplateManager`."""
# Just record the fact that we have a template set. Make sure that if
# there is an existing template with the same context and name, we
# override any of its settings (and evict the cache).
template = store.query(Template).filter(
Template.name == name,
Template.context == context).one_or_none()
if template is None:
template = Template(name, context, uri, username, password)
store.add(template)
else:
template.reset(uri, username, password)
@dbconnection
def get(self, store, name, context, **kws):
"""See `ITemplateManager`."""
template = store.query(Template).filter(
Template.name == name,
Template.context == context).one_or_none()
if template is None:
return None
actual_uri = expand(template.uri, None, kws)
cache_mgr = getUtility(ICacheManager)
contents = cache_mgr.get(actual_uri)
if contents is None:
# It's likely that the cached contents have expired.
auth = {}
if template.username is not None:
auth['auth'] = (template.username, template.password)
try:
contents = protocols.get(actual_uri, **auth)
except HTTPError as error:
# 404/NotFound errors are interpreted as missing templates,
# for which we'll return the default (i.e. the empty string).
# All other exceptions get passed up the chain.
if error.response.status_code != 404:
raise
log.exception('Cannot retrieve template at {} ({})'.format(
actual_uri, auth.get('auth', '<no authorization>')))
return ''
# We don't need to cache mailman: contents since those are already
# on the file system.
if urlparse(actual_uri).scheme != 'mailman':
cache_mgr.add(actual_uri, contents)
return contents
@dbconnection
def raw(self, store, name, context):
"""See `ITemplateManager`."""
return store.query(Template).filter(
Template.name == name,
Template.context == context).one_or_none()
@dbconnection
def delete(self, store, name, context):
"""See `ITemplateManager`."""
template = store.query(Template).filter(
Template.name == name,
Template.context == context).one_or_none()
if template is not None:
store.delete(template)
# We don't clear the cache entry, we just let it expire.
@public
@implementer(ITemplateLoader)
class TemplateLoader:
"""Loader of templates."""
def get(self, name, context=None, **kws):
"""See `ITemplateLoader`."""
# Gather some additional information based on the context.
substitutions = {}
if IMailingList.providedBy(context):
mlist = context
domain = context.domain
lookup_contexts = [
mlist.list_id,
mlist.mail_host,
None,
]
substitutions.update(dict(
list_id=mlist.list_id,
# For backward compatibility, we call this $listname.
listname=mlist.fqdn_listname,
domain_name=domain.mail_host,
language=mlist.preferred_language.code,
))
elif IDomain.providedBy(context):
mlist = None
domain = context
lookup_contexts = [
domain.mail_host,
None,
]
substitutions['domain_name'] = domain.mail_host
elif context is None:
mlist = domain = None
lookup_contexts = [None]
else:
raise ValueError('Bad context type: {!r}'.format(context))
# The passed in keyword arguments take precedence.
substitutions.update(kws)
# See if there's a cached template registered for this name and
# context, passing in the url substitutions. This handles http:,
# https:, and file: urls.
for lookup_context in lookup_contexts:
try:
contents = getUtility(ITemplateManager).get(
name, lookup_context, **substitutions)
except (HTTPError, URLError):
pass
else:
if contents is not None:
return contents
# Fallback to searching within the source code.
code = substitutions.get('language', config.mailman.default_language)
# Find the template, mutating any missing template exception.
missing = object()
default_uri = ALL_TEMPLATES.get(name, missing)
if default_uri is None:
return ''
elif default_uri is missing:
raise URLError('No such file')
path, fp = find(default_uri, mlist, code)
try:
return fp.read()
finally:
fp.close()
|