summaryrefslogtreecommitdiff
path: root/src/mailman/model/template.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/model/template.py')
-rw-r--r--src/mailman/model/template.py202
1 files changed, 202 insertions, 0 deletions
diff --git a/src/mailman/model/template.py b/src/mailman/model/template.py
new file mode 100644
index 000000000..96ce43e1f
--- /dev/null
+++ b/src/mailman/model/template.py
@@ -0,0 +1,202 @@
+# 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.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, Unicode
+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(Unicode)
+ context = Column(Unicode)
+ uri = Column(Unicode)
+ username = Column(Unicode, nullable=True)
+ password = Column(Unicode, 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()