From 372c13dcbdde1ca18d192236fc5ac132bd9d82f3 Mon Sep 17 00:00:00 2001
From: liuyanglinux
Date: Thu, 12 Jan 2023 19:26:00 +0800
Subject: [PATCH] update opengauss mailweb/mailcore/exim
---
deploy/mail/config.yaml | 3515 -------------------------------
deploy/mail/exim-configmap.yaml | 1015 +++++++++
deploy/mail/ingress.yaml | 26 +
deploy/mail/kustomization.yaml | 14 +
deploy/mail/mailcore-utils.yaml | 82 -
deploy/mail/mailcore.yaml | 97 +-
deploy/mail/mtaservice.yaml | 66 +-
deploy/mail/nginx-config.yaml | 60 +
deploy/mail/secrets.yaml | 51 +-
deploy/mail/storage.yaml | 14 +
deploy/mail/webservice.yaml | 152 +-
11 files changed, 1297 insertions(+), 3795 deletions(-)
delete mode 100644 deploy/mail/config.yaml
create mode 100644 deploy/mail/exim-configmap.yaml
create mode 100644 deploy/mail/ingress.yaml
create mode 100644 deploy/mail/kustomization.yaml
delete mode 100644 deploy/mail/mailcore-utils.yaml
create mode 100644 deploy/mail/nginx-config.yaml
diff --git a/deploy/mail/config.yaml b/deploy/mail/config.yaml
deleted file mode 100644
index 3ab5ac9b..00000000
--- a/deploy/mail/config.yaml
+++ /dev/null
@@ -1,3515 +0,0 @@
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: mailman-core-configmap
- namespace: mail
-data:
- #patch subscriptions.py for unsubscription without confirm
- subscriptions.py: |
- # Copyright (C) 2009-2019 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 .
-
- """Handle subscriptions."""
-
- import uuid
- import logging
-
- from datetime import timedelta
- from email.utils import formataddr
- from enum import Enum
- from mailman.app.membership import delete_member
- from mailman.app.workflow import Workflow
- from mailman.core.i18n import _
- from mailman.database.transaction import flush
- from mailman.email.message import UserNotification
- from mailman.interfaces.address import IAddress
- from mailman.interfaces.bans import IBanManager
- from mailman.interfaces.listmanager import ListDeletingEvent
- from mailman.interfaces.mailinglist import SubscriptionPolicy
- from mailman.interfaces.member import (
- AlreadySubscribedError, MemberRole, MembershipIsBannedError,
- NotAMemberError)
- from mailman.interfaces.pending import IPendable, IPendings
- from mailman.interfaces.subscriptions import (
- ISubscriptionManager, ISubscriptionService,
- SubscriptionConfirmationNeededEvent, SubscriptionPendingError, TokenOwner,
- UnsubscriptionConfirmationNeededEvent)
- from mailman.interfaces.template import ITemplateLoader
- from mailman.interfaces.user import IUser
- from mailman.interfaces.usermanager import IUserManager
- from mailman.interfaces.workflow import IWorkflowStateManager
- from mailman.utilities.datetime import now
- from mailman.utilities.string import expand, wrap
- from public import public
- from zope.component import getUtility
- from zope.event import notify
- from zope.interface import implementer
-
- log = logging.getLogger('mailman.subscribe')
-
-
- class WhichSubscriber(Enum):
- address = 1
- user = 2
-
- @implementer(IPendable)
- class PendableSubscription(dict):
- PEND_TYPE = 'subscription'
-
-
- @implementer(IPendable)
- class PendableUnsubscription(dict):
- PEND_TYPE = 'unsubscription'
-
-
- class _SubscriptionWorkflowCommon(Workflow):
- """Common support between subscription and unsubscription."""
-
- PENDABLE_CLASS = None
-
- def __init__(self, mlist, subscriber):
- super().__init__()
- self.mlist = mlist
- self.address = None
- self.user = None
- self.which = None
- self.member = None
- self._set_token(TokenOwner.no_one)
- # The subscriber must be either an IUser or IAddress.
- if IAddress.providedBy(subscriber):
- self.address = subscriber
- self.user = self.address.user
- self.which = WhichSubscriber.address
- elif IUser.providedBy(subscriber):
- self.address = subscriber.preferred_address
- self.user = subscriber
- self.which = WhichSubscriber.user
- self.subscriber = subscriber
-
- @property
- def user_key(self):
- # For save.
- return self.user.user_id.hex
-
- @user_key.setter
- def user_key(self, hex_key):
- # For restore.
- uid = uuid.UUID(hex_key)
- self.user = getUtility(IUserManager).get_user_by_id(uid)
- if self.user is None:
- self.user = self.address.user
-
- @property
- def address_key(self):
- # For save.
- return self.address.email
-
- @address_key.setter
- def address_key(self, email):
- # For restore.
- self.address = getUtility(IUserManager).get_address(email)
- assert self.address is not None
-
- @property
- def subscriber_key(self):
- return self.which.value
-
- @subscriber_key.setter
- def subscriber_key(self, key):
- self.which = WhichSubscriber(key)
-
- @property
- def token_owner_key(self):
- return self.token_owner.value
-
- @token_owner_key.setter
- def token_owner_key(self, value):
- self.token_owner = TokenOwner(value)
-
- def _set_token(self, token_owner):
- assert isinstance(token_owner, TokenOwner)
- pendings = getUtility(IPendings)
- # Clear out the previous pending token if there is one.
- if self.token is not None:
- pendings.confirm(self.token)
- # Create a new token to prevent replay attacks. It seems like this
- # would produce the same token, but it won't because the pending adds a
- # bit of randomization.
- self.token_owner = token_owner
- if token_owner is TokenOwner.no_one:
- self.token = None
- return
- pendable = self.PENDABLE_CLASS(
- list_id=self.mlist.list_id,
- email=self.address.email,
- display_name=self.address.display_name,
- when=now().replace(microsecond=0).isoformat(),
- token_owner=token_owner.name,
- )
- self.token = pendings.add(pendable, timedelta(days=3650))
-
- @public
- class SubscriptionWorkflow(_SubscriptionWorkflowCommon):
- """Workflow of a subscription request."""
-
- PENDABLE_CLASS = PendableSubscription
- INITIAL_STATE = 'sanity_checks'
- SAVE_ATTRIBUTES = (
- 'pre_approved',
- 'pre_confirmed',
- 'pre_verified',
- 'address_key',
- 'subscriber_key',
- 'user_key',
- 'token_owner_key',
- )
-
- def __init__(self, mlist, subscriber=None, *,
- pre_verified=False, pre_confirmed=False, pre_approved=False):
- super().__init__(mlist, subscriber)
- self.pre_verified = pre_verified
- self.pre_confirmed = pre_confirmed
- self.pre_approved = pre_approved
-
- def _step_sanity_checks(self):
- # Ensure that we have both an address and a user, even if the address
- # is not verified. We can't set the preferred address until it is
- # verified.
- if self.user is None:
- # The address has no linked user so create one, link it, and set
- # the user's preferred address.
- assert self.address is not None, 'No address or user'
- self.user = getUtility(IUserManager).make_user(self.address.email)
- if self.address is None:
- assert self.user.preferred_address is None, (
- "Preferred address exists, but wasn't used in constructor")
- addresses = list(self.user.addresses)
- if len(addresses) == 0:
- raise AssertionError('User has no addresses: {}'.format(
- self.user))
- # This is rather arbitrary, but we have no choice.
- self.address = addresses[0]
- assert self.user is not None and self.address is not None, (
- 'Insane sanity check results')
- # Is this subscriber already a member?
- if (self.which is WhichSubscriber.user and
- self.user.preferred_address is not None):
- subscriber = self.user
- else:
- subscriber = self.address
- if self.mlist.is_subscribed(subscriber):
- # 2017-04-22 BAW: This branch actually *does* get covered, as I've
- # verified by a full coverage run, but diffcov for some reason
- # claims that the test added in the branch that added this code
- # does not cover the change. That seems like a bug in diffcov.
- raise AlreadySubscribedError( # pragma: nocover
- self.mlist.fqdn_listname,
- self.address.email,
- MemberRole.member)
- # Is this email address banned?
- if IBanManager(self.mlist).is_banned(self.address.email):
- raise MembershipIsBannedError(self.mlist, self.address.email)
- # Check if there is already a subscription request for this email.
- pendings = getUtility(IPendings).find(
- mlist=self.mlist,
- pend_type='subscription')
- for token, pendable in pendings:
- if pendable['email'] == self.address.email:
- raise SubscriptionPendingError(self.mlist, self.address.email)
- # Start out with the subscriber being the token owner.
- self.push('verification_checks')
-
- def _step_verification_checks(self):
- # Is the address already verified, or is the pre-verified flag set?
- if self.address.verified_on is None:
- if self.pre_verified:
- self.address.verified_on = now()
- else:
- # The address being subscribed is not yet verified, so we need
- # to send a validation email that will also confirm that the
- # user wants to be subscribed to this mailing list.
- self.push('send_confirmation')
- return
- self.push('confirmation_checks')
-
- def _step_confirmation_checks(self):
- # If the list's subscription policy is open, then the user can be
- # subscribed right here and now.
- if self.mlist.subscription_policy is SubscriptionPolicy.open:
- self.push('do_subscription')
- return
- # If we do not need the user's confirmation, then skip to the
- # moderation checks.
- if self.mlist.subscription_policy is SubscriptionPolicy.moderate:
- self.push('moderation_checks')
- return
- # If the subscription has been pre-confirmed, then we can skip the
- # confirmation check can be skipped. If moderator approval is
- # required we need to check that, otherwise we can go straight to
- # subscription.
- if self.pre_confirmed:
- next_step = (
- 'moderation_checks'
- if self.mlist.subscription_policy is
- SubscriptionPolicy.confirm_then_moderate # noqa: E131
- else 'do_subscription')
- self.push(next_step)
- return
- # The user must confirm their subscription.
- self.push('send_confirmation')
-
- def _step_moderation_checks(self):
- # Does the moderator need to approve the subscription request?
- assert self.mlist.subscription_policy in (
- SubscriptionPolicy.moderate,
- SubscriptionPolicy.confirm_then_moderate,
- ), self.mlist.subscription_policy
- if self.pre_approved:
- self.push('do_subscription')
- else:
- self.push('get_moderator_approval')
-
- def _step_get_moderator_approval(self):
- # Here's the next step in the workflow, assuming the moderator
- # approves of the subscription. If they don't, the workflow and
- # subscription request will just be thrown away.
- self._set_token(TokenOwner.moderator)
- self.push('subscribe_from_restored')
- self.save()
- log.info('{}: held subscription request from {}'.format(
- self.mlist.fqdn_listname, self.address.email))
- # Possibly send a notification to the list moderators.
- if self.mlist.admin_immed_notify:
- subject = _(
- 'New subscription request to $self.mlist.display_name '
- 'from $self.address.email')
- username = formataddr(
- (self.subscriber.display_name, self.address.email))
- template = getUtility(ITemplateLoader).get(
- 'list:admin:action:subscribe', self.mlist)
- text = wrap(expand(template, self.mlist, dict(
- member=username,
- )))
- # This message should appear to come from the -owner so as
- # to avoid any useless bounce processing.
- msg = UserNotification(
- self.mlist.owner_address, self.mlist.owner_address,
- subject, text, self.mlist.preferred_language)
- msg.send(self.mlist)
- # The workflow must stop running here.
- raise StopIteration
-
- def _step_subscribe_from_restored(self):
- # Prevent replay attacks.
- self._set_token(TokenOwner.no_one)
- # Restore a little extra state that can't be stored in the database
- # (because the order of setattr() on restore is indeterminate), then
- # subscribe the user.
- if self.which is WhichSubscriber.address:
- self.subscriber = self.address
- else:
- assert self.which is WhichSubscriber.user
- self.subscriber = self.user
- self.push('do_subscription')
-
- def _step_do_subscription(self):
- # We can immediately subscribe the user to the mailing list.
- self.member = self.mlist.subscribe(self.subscriber)
- assert self.token is None and self.token_owner is TokenOwner.no_one, (
- 'Unexpected active token at end of subscription workflow')
-
- def _step_send_confirmation(self):
- self._set_token(TokenOwner.subscriber)
- self.push('do_confirm_verify')
- self.save()
- # Triggering this event causes the confirmation message to be sent.
- notify(SubscriptionConfirmationNeededEvent(
- self.mlist, self.token, self.address.email))
- # Now we wait for the confirmation.
- raise StopIteration
-
- def _step_do_confirm_verify(self):
- # Restore a little extra state that can't be stored in the database
- # (because the order of setattr() on restore is indeterminate), then
- # continue with the confirmation/verification step.
- if self.which is WhichSubscriber.address:
- self.subscriber = self.address
- else:
- assert self.which is WhichSubscriber.user
- self.subscriber = self.user
- # Reset the token so it can't be used in a replay attack.
- self._set_token(TokenOwner.no_one)
- # The user has confirmed their subscription request, and also verified
- # their email address if necessary. This latter needs to be set on the
- # IAddress, but there's nothing more to do about the confirmation step.
- # We just continue along with the workflow.
- if self.address.verified_on is None:
- self.address.verified_on = now()
- # The next step depends on the mailing list's subscription policy.
- next_step = ('moderation_checks'
- if self.mlist.subscription_policy in (
- SubscriptionPolicy.moderate,
- SubscriptionPolicy.confirm_then_moderate,
- )
- else 'do_subscription')
- self.push(next_step)
-
- @public
- class UnSubscriptionWorkflow(_SubscriptionWorkflowCommon):
- """Workflow of a unsubscription request."""
-
- PENDABLE_CLASS = PendableUnsubscription
- INITIAL_STATE = 'subscription_checks'
- SAVE_ATTRIBUTES = (
- 'pre_approved',
- 'pre_confirmed',
- 'address_key',
- 'user_key',
- 'subscriber_key',
- 'token_owner_key',
- )
-
- def __init__(self, mlist, subscriber=None, *,
- pre_approved=False, pre_confirmed=False):
- super().__init__(mlist, subscriber)
- if IAddress.providedBy(subscriber) or IUser.providedBy(subscriber):
- self.member = self.mlist.regular_members.get_member(
- self.address.email)
- self.pre_confirmed = pre_confirmed
- self.pre_approved = pre_approved
-
- def _step_subscription_checks(self):
- assert self.mlist.is_subscribed(self.subscriber)
- self.push('confirmation_checks')
-
- def _step_confirmation_checks(self):
- # If list's unsubscription policy is open, the user can unsubscribe
- # right now.
- #if self.mlist.unsubscription_policy is SubscriptionPolicy.open :
- self.push('do_unsubscription')
- return
- # If we don't need the user's confirmation, then skip to the moderation
- # checks.
- # if self.mlist.unsubscription_policy is SubscriptionPolicy.moderate:
- # self.push('moderation_checks')
- # return
- # If the request is pre-confirmed, then the user can unsubscribe right
- # now.
- # if self.pre_confirmed:
- # self.push('do_unsubscription')
- # return
- # The user must confirm their un-subsbcription.
- # self.push('send_confirmation')
-
- def _step_send_confirmation(self):
- self._set_token(TokenOwner.subscriber)
- self.push('do_confirm_verify')
- self.save()
- notify(UnsubscriptionConfirmationNeededEvent(
- self.mlist, self.token, self.address.email))
- raise StopIteration
-
- def _step_moderation_checks(self):
- # Does the moderator need to approve the unsubscription request?
- assert self.mlist.unsubscription_policy in (
- SubscriptionPolicy.moderate,
- SubscriptionPolicy.confirm_then_moderate,
- ), self.mlist.unsubscription_policy
- if self.pre_approved:
- self.push('do_unsubscription')
- else:
- self.push('get_moderator_approval')
-
- def _step_get_moderator_approval(self):
- self._set_token(TokenOwner.moderator)
- self.push('unsubscribe_from_restored')
- self.save()
- log.info('{}: held unsubscription request from {}'.format(
- self.mlist.fqdn_listname, self.address.email))
- if self.mlist.admin_immed_notify:
- subject = _(
- 'New unsubscription request to $self.mlist.display_name '
- 'from $self.address.email')
- username = formataddr(
- (self.subscriber.display_name, self.address.email))
- template = getUtility(ITemplateLoader).get(
- 'list:admin:action:unsubscribe', self.mlist)
- text = wrap(expand(template, self.mlist, dict(
- member=username,
- )))
- # This message should appear to come from the -owner so as
- # to avoid any useless bounce processing.
- msg = UserNotification(
- self.mlist.owner_address, self.mlist.owner_address,
- subject, text, self.mlist.preferred_language)
- msg.send(self.mlist)
- # The workflow must stop running here
- raise StopIteration
-
- def _step_do_confirm_verify(self):
- # Restore a little extra state that can't be stored in the database
- # (because the order of setattr() on restore is indeterminate), then
- # continue with the confirmation/verification step.
- if self.which is WhichSubscriber.address:
- self.subscriber = self.address
- else:
- assert self.which is WhichSubscriber.user
- self.subscriber = self.user
- # Reset the token so it can't be used in a replay attack.
- self._set_token(TokenOwner.no_one)
- # Restore the member object.
- self.member = self.mlist.regular_members.get_member(self.address.email)
- # It's possible the member was already unsubscribed while we were
- # waiting for the confirmation.
- if self.member is None:
- return
- # The user has confirmed their unsubscription request
- next_step = ('moderation_checks'
- if self.mlist.unsubscription_policy in (
- SubscriptionPolicy.moderate,
- SubscriptionPolicy.confirm_then_moderate,
- )
- else 'do_unsubscription')
- self.push(next_step)
-
- def _step_do_unsubscription(self):
- try:
- delete_member(self.mlist, self.address.email)
- except NotAMemberError:
- # The member has already been unsubscribed.
- pass
- self.member = None
- assert self.token is None and self.token_owner is TokenOwner.no_one, (
- 'Unexpected active token at end of subscription workflow')
-
- def _step_unsubscribe_from_restored(self):
- # Prevent replay attacks.
- self._set_token(TokenOwner.no_one)
- if self.which is WhichSubscriber.address:
- self.subscriber = self.address
- else:
- assert self.which is WhichSubscriber.user
- self.subscriber = self.user
- self.push('do_unsubscription')
-
- @public
- @implementer(ISubscriptionManager)
- class SubscriptionManager:
- def __init__(self, mlist):
- self._mlist = mlist
-
- def register(self, subscriber=None, *,
- pre_verified=False, pre_confirmed=False, pre_approved=False):
- """See `ISubscriptionManager`."""
- workflow = SubscriptionWorkflow(
- self._mlist, subscriber,
- pre_verified=pre_verified,
- pre_confirmed=pre_confirmed,
- pre_approved=pre_approved)
- list(workflow)
- return workflow.token, workflow.token_owner, workflow.member
-
- def unregister(self, subscriber=None, *,
- pre_confirmed=False, pre_approved=False):
- workflow = UnSubscriptionWorkflow(
- self._mlist, subscriber,
- pre_confirmed=pre_confirmed,
- pre_approved=pre_approved)
- list(workflow)
- return workflow.token, workflow.token_owner, workflow.member
-
- def confirm(self, token):
- if token is None:
- raise LookupError
- pendable = getUtility(IPendings).confirm(token, expunge=False)
- if pendable is None:
- raise LookupError
- workflow_type = pendable.get('type')
- assert workflow_type in (PendableSubscription.PEND_TYPE,
- PendableUnsubscription.PEND_TYPE)
- workflow = (SubscriptionWorkflow
- if workflow_type == PendableSubscription.PEND_TYPE
- else UnSubscriptionWorkflow)(self._mlist)
- workflow.token = token
- workflow.restore()
- # In order to just run the whole workflow, all we need to do
- # is iterate over the workflow object. On calling the __next__
- # over the workflow iterator it automatically executes the steps
- # that needs to be done.
- list(workflow)
- return workflow.token, workflow.token_owner, workflow.member
-
- def discard(self, token):
- with flush():
- getUtility(IPendings).confirm(token)
- getUtility(IWorkflowStateManager).discard(token)
-
- def _handle_confirmation_needed_events(event, template_name):
- subject = 'confirm {}'.format(event.token)
- confirm_address = event.mlist.confirm_address(event.token)
- email_address = event.email
- # Send a verification email to the address.
- template = getUtility(ITemplateLoader).get(template_name, event.mlist)
- text = expand(template, event.mlist, dict(
- token=event.token,
- subject=subject,
- confirm_email=confirm_address,
- user_email=email_address,
- # For backward compatibility.
- confirm_address=confirm_address,
- email_address=email_address,
- domain_name=event.mlist.domain.mail_host,
- contact_address=event.mlist.owner_address,
- ))
- msg = UserNotification(
- email_address, confirm_address, subject, text,
- event.mlist.preferred_language)
- msg.send(event.mlist, add_precedence=False)
-
- @public
- def handle_SubscriptionConfirmationNeededEvent(event):
- if not isinstance(event, SubscriptionConfirmationNeededEvent):
- return
- _handle_confirmation_needed_events(event, 'list:user:action:subscribe')
-
- @public
- def handle_UnsubscriptionConfirmationNeededEvent(event):
- if not isinstance(event, UnsubscriptionConfirmationNeededEvent):
- return
- _handle_confirmation_needed_events(event, 'list:user:action:unsubscribe')
-
- @public
- def handle_ListDeletingEvent(event):
- """Delete a mailing list's members when the list is being deleted."""
-
- if not isinstance(event, ListDeletingEvent):
- return
- # Find all the members still associated with the mailing list.
- members = getUtility(ISubscriptionService).find_members(
- list_id=event.mailing_list.list_id)
- for member in members:
- member.unsubscribe()
-
- #patch base64mime.py for the purpose of encoding fix at: https://bugs.python.org/issue44560
- base64mime.py: |
- # Copyright (C) 2002-2007 Python Software Foundation
- # Author: Ben Gertzfield
- # Contact: email-sig@python.org
- """Base64 content transfer encoding per RFCs 2045-2047.
- This module handles the content transfer encoding method defined in RFC 2045
- to encode arbitrary 8-bit data using the three 8-bit bytes in four 7-bit
- characters encoding known as Base64.
- It is used in the MIME standards for email to attach images, audio, and text
- using some 8-bit character sets to messages.
- This module provides an interface to encode and decode both headers and bodies
- with Base64 encoding.
- RFC 2045 defines a method for including character set information in an
- `encoded-word' in a header. This method is commonly used for 8-bit real names
- in To:, From:, Cc:, etc. fields, as well as Subject: lines.
- This module does not do the line wrapping or end-of-line character conversion
- necessary for proper internationalized headers; it only does dumb encoding and
- decoding. To deal with the various line wrapping issues, use the email.header
- module.
- """
- __all__ = [
- 'body_decode',
- 'body_encode',
- 'decode',
- 'decodestring',
- 'header_encode',
- 'header_length',
- ]
- from base64 import b64encode
- from binascii import b2a_base64, a2b_base64
- CRLF = '\r\n'
- NL = '\n'
- EMPTYSTRING = ''
- # See also Charset.py
- MISC_LEN = 7
- # Helpers
- def header_length(bytearray):
- """Return the length of s when it is encoded with base64."""
- groups_of_3, leftover = divmod(len(bytearray), 3)
- # 4 bytes out for each 3 bytes (or nonzero fraction thereof) in.
- n = groups_of_3 * 4
- if leftover:
- n += 4
- return n
- def header_encode(header_bytes, charset='iso-8859-1'):
- """Encode a single header line with Base64 encoding in a given charset.
- charset names the character set to use to encode the header. It defaults
- to iso-8859-1. Base64 encoding is defined in RFC 2045.
- """
- if not header_bytes:
- return ""
- if isinstance(header_bytes, str):
- header_bytes = header_bytes.encode(charset)
- encoded = b64encode(header_bytes).decode("ascii")
- if charset == "eucgb2312_cn":
- charset = "gb2312"
- return '=?%s?b?%s?=' % (charset, encoded)
- def body_encode(s, maxlinelen=76, eol=NL):
- r"""Encode a string with base64.
- Each line will be wrapped at, at most, maxlinelen characters (defaults to
- 76 characters).
- Each line of encoded text will end with eol, which defaults to "\n". Set
- this to "\r\n" if you will be using the result of this function directly
- in an email.
- """
- if not s:
- return s
- encvec = []
- max_unencoded = maxlinelen * 3 // 4
- for i in range(0, len(s), max_unencoded):
- # BAW: should encode() inherit b2a_base64()'s dubious behavior in
- # adding a newline to the encoded string?
- enc = b2a_base64(s[i:i + max_unencoded]).decode("ascii")
- if enc.endswith(NL) and eol != NL:
- enc = enc[:-1] + eol
- encvec.append(enc)
- return EMPTYSTRING.join(encvec)
- def decode(string):
- """Decode a raw base64 string, returning a bytes object.
- This function does not parse a full MIME header value encoded with
- base64 (like =?iso-8859-1?b?bmloISBuaWgh?=) -- please use the high
- level email.header class for that functionality.
- """
- if not string:
- return bytes()
- elif isinstance(string, str):
- return a2b_base64(string.encode('raw-unicode-escape'))
- else:
- return a2b_base64(string)
- # For convenience and backwards compatibility w/ standard base64 module
- body_decode = decode
- decodestring = decode
- # patch base64mime.py for the purpose of encoding fix at: https://bugs.python.org/issue44560
- quoprimime.py: |
- # Copyright (C) 2001-2006 Python Software Foundation
- # Author: Ben Gertzfield
- # Contact: email-sig@python.org
- """Quoted-printable content transfer encoding per RFCs 2045-2047.
- This module handles the content transfer encoding method defined in RFC 2045
- to encode US ASCII-like 8-bit data called `quoted-printable'. It is used to
- safely encode text that is in a character set similar to the 7-bit US ASCII
- character set, but that includes some 8-bit characters that are normally not
- allowed in email bodies or headers.
- Quoted-printable is very space-inefficient for encoding binary files; use the
- email.base64mime module for that instead.
- This module provides an interface to encode and decode both headers and bodies
- with quoted-printable encoding.
- RFC 2045 defines a method for including character set information in an
- `encoded-word' in a header. This method is commonly used for 8-bit real names
- in To:/From:/Cc: etc. fields, as well as Subject: lines.
- This module does not do the line wrapping or end-of-line character
- conversion necessary for proper internationalized headers; it only
- does dumb encoding and decoding. To deal with the various line
- wrapping issues, use the email.header module.
- """
- __all__ = [
- 'body_decode',
- 'body_encode',
- 'body_length',
- 'decode',
- 'decodestring',
- 'header_decode',
- 'header_encode',
- 'header_length',
- 'quote',
- 'unquote',
- ]
- import re
- from string import ascii_letters, digits, hexdigits
- CRLF = '\r\n'
- NL = '\n'
- EMPTYSTRING = ''
- # Build a mapping of octets to the expansion of that octet. Since we're only
- # going to have 256 of these things, this isn't terribly inefficient
- # space-wise. Remember that headers and bodies have different sets of safe
- # characters. Initialize both maps with the full expansion, and then override
- # the safe bytes with the more compact form.
- _QUOPRI_MAP = ['=%02X' % c for c in range(256)]
- _QUOPRI_HEADER_MAP = _QUOPRI_MAP[:]
- _QUOPRI_BODY_MAP = _QUOPRI_MAP[:]
- # Safe header bytes which need no encoding.
- for c in b'-!*+/' + ascii_letters.encode('ascii') + digits.encode('ascii'):
- _QUOPRI_HEADER_MAP[c] = chr(c)
- # Headers have one other special encoding; spaces become underscores.
- _QUOPRI_HEADER_MAP[ord(' ')] = '_'
- # Safe body bytes which need no encoding.
- for c in (b' !"#$%&\'()*+,-./0123456789:;<>'
- b'?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`'
- b'abcdefghijklmnopqrstuvwxyz{|}~\t'):
- _QUOPRI_BODY_MAP[c] = chr(c)
- # Helpers
- def header_check(octet):
- """Return True if the octet should be escaped with header quopri."""
- return chr(octet) != _QUOPRI_HEADER_MAP[octet]
- def body_check(octet):
- """Return True if the octet should be escaped with body quopri."""
- return chr(octet) != _QUOPRI_BODY_MAP[octet]
- def header_length(bytearray):
- """Return a header quoted-printable encoding length.
- Note that this does not include any RFC 2047 chrome added by
- `header_encode()`.
- :param bytearray: An array of bytes (a.k.a. octets).
- :return: The length in bytes of the byte array when it is encoded with
- quoted-printable for headers.
- """
- return sum(len(_QUOPRI_HEADER_MAP[octet]) for octet in bytearray)
- def body_length(bytearray):
- """Return a body quoted-printable encoding length.
- :param bytearray: An array of bytes (a.k.a. octets).
- :return: The length in bytes of the byte array when it is encoded with
- quoted-printable for bodies.
- """
- return sum(len(_QUOPRI_BODY_MAP[octet]) for octet in bytearray)
- def _max_append(L, s, maxlen, extra=''):
- if not isinstance(s, str):
- s = chr(s)
- if not L:
- L.append(s.lstrip())
- elif len(L[-1]) + len(s) <= maxlen:
- L[-1] += extra + s
- else:
- L.append(s.lstrip())
- def unquote(s):
- """Turn a string in the form =AB to the ASCII character with value 0xab"""
- return chr(int(s[1:3], 16))
- def quote(c):
- return _QUOPRI_MAP[ord(c)]
- def header_encode(header_bytes, charset='iso-8859-1'):
- """Encode a single header line with quoted-printable (like) encoding.
- Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but
- used specifically for email header fields to allow charsets with mostly 7
- bit characters (and some 8 bit) to remain more or less readable in non-RFC
- 2045 aware mail clients.
- charset names the character set to use in the RFC 2046 header. It
- defaults to iso-8859-1.
- """
- # Return empty headers as an empty string.
- if not header_bytes:
- return ''
- # Iterate over every byte, encoding if necessary.
- encoded = header_bytes.decode('latin1').translate(_QUOPRI_HEADER_MAP)
- # Now add the RFC chrome to each encoded chunk and glue the chunks
- # together.
- if charset == "eucgb2312_cn":
- charset = "gb2312"
- return '=?%s?q?%s?=' % (charset, encoded)
- _QUOPRI_BODY_ENCODE_MAP = _QUOPRI_BODY_MAP[:]
- for c in b'\r\n':
- _QUOPRI_BODY_ENCODE_MAP[c] = chr(c)
- def body_encode(body, maxlinelen=76, eol=NL):
- """Encode with quoted-printable, wrapping at maxlinelen characters.
- Each line of encoded text will end with eol, which defaults to "\\n". Set
- this to "\\r\\n" if you will be using the result of this function directly
- in an email.
- Each line will be wrapped at, at most, maxlinelen characters before the
- eol string (maxlinelen defaults to 76 characters, the maximum value
- permitted by RFC 2045). Long lines will have the 'soft line break'
- quoted-printable character "=" appended to them, so the decoded text will
- be identical to the original text.
- The minimum maxlinelen is 4 to have room for a quoted character ("=XX")
- followed by a soft line break. Smaller values will generate a
- ValueError.
- """
- if maxlinelen < 4:
- raise ValueError("maxlinelen must be at least 4")
- if not body:
- return body
- # quote special characters
- body = body.translate(_QUOPRI_BODY_ENCODE_MAP)
- soft_break = '=' + eol
- # leave space for the '=' at the end of a line
- maxlinelen1 = maxlinelen - 1
- encoded_body = []
- append = encoded_body.append
- for line in body.splitlines():
- # break up the line into pieces no longer than maxlinelen - 1
- start = 0
- laststart = len(line) - 1 - maxlinelen
- while start <= laststart:
- stop = start + maxlinelen1
- # make sure we don't break up an escape sequence
- if line[stop - 2] == '=':
- append(line[start:stop - 1])
- start = stop - 2
- elif line[stop - 1] == '=':
- append(line[start:stop])
- start = stop - 1
- else:
- append(line[start:stop] + '=')
- start = stop
- # handle rest of line, special case if line ends in whitespace
- if line and line[-1] in ' \t':
- room = start - laststart
- if room >= 3:
- # It's a whitespace character at end-of-line, and we have room
- # for the three-character quoted encoding.
- q = quote(line[-1])
- elif room == 2:
- # There's room for the whitespace character and a soft break.
- q = line[-1] + soft_break
- else:
- # There's room only for a soft break. The quoted whitespace
- # will be the only content on the subsequent line.
- q = soft_break + quote(line[-1])
- append(line[start:-1] + q)
- else:
- append(line[start:])
- # add back final newline if present
- if body[-1] in CRLF:
- append('')
- return eol.join(encoded_body)
- # BAW: I'm not sure if the intent was for the signature of this function to be
- # the same as base64MIME.decode() or not...
- def decode(encoded, eol=NL):
- """Decode a quoted-printable string.
- Lines are separated with eol, which defaults to \\n.
- """
- if not encoded:
- return encoded
- # BAW: see comment in encode() above. Again, we're building up the
- # decoded string with string concatenation, which could be done much more
- # efficiently.
- decoded = ''
- for line in encoded.splitlines():
- line = line.rstrip()
- if not line:
- decoded += eol
- continue
- i = 0
- n = len(line)
- while i < n:
- c = line[i]
- if c != '=':
- decoded += c
- i += 1
- # Otherwise, c == "=". Are we at the end of the line? If so, add
- # a soft line break.
- elif i+1 == n:
- i += 1
- continue
- # Decode if in form =AB
- elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits:
- decoded += unquote(line[i:i+3])
- i += 3
- # Otherwise, not in form =AB, pass literally
- else:
- decoded += c
- i += 1
- if i == n:
- decoded += eol
- # Special case if original string did not end with eol
- if encoded[-1] not in '\r\n' and decoded.endswith(eol):
- decoded = decoded[:-1]
- return decoded
- # For convenience and backwards compatibility w/ standard base64 module
- body_decode = decode
- decodestring = decode
- def _unquote_match(match):
- """Turn a match in the form =AB to the ASCII character with value 0xab"""
- s = match.group(0)
- return unquote(s)
- # Header decoding is done a bit differently
- def header_decode(s):
- """Decode a string encoded with RFC 2045 MIME header `Q' encoding.
- This function does not parse a full MIME header value encoded with
- quoted-printable (like =?iso-8859-1?q?Hello_World?=) -- please use
- the high level email.header class for that functionality.
- """
- s = s.replace('_', ' ')
- return re.sub(r'=[a-fA-F0-9]{2}', _unquote_match, s, flags=re.ASCII)
- command.py: |
- # Copyright (C) 1998-2021 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 .
-
- """-request robot command runner."""
-
- # See the delivery diagram in IncomingRunner.py. This module handles all
- # email destined for mylist-request, -join, and -leave. It no longer handles
- # bounce messages (i.e. -admin or -bounces), nor does it handle mail to
- # -owner.
-
- import base64
- import re
- import logging
-
- from contextlib import suppress
- from email.errors import HeaderParseError
- from email.header import decode_header, make_header
- from email.iterators import typed_subpart_iterator
- from io import StringIO
- from mailman.config import config
- from mailman.core.i18n import _
- from mailman.core.runner import Runner
- from mailman.email.message import UserNotification
- from mailman.interfaces.autorespond import ResponseAction
- from mailman.interfaces.command import ContinueProcessing, IEmailResults
- from mailman.interfaces.languages import ILanguageManager
- from public import public
- from zope.component import getUtility
- from zope.interface import implementer
-
-
- NL = '\n'
- log = logging.getLogger('mailman.vette')
- class CommandFinder:
- """Generate commands from the content of a message."""
- def __init__(self, msg, msgdata, results):
- self.command_lines = []
- self.ignored_lines = []
- self.processed_lines = []
- self.send_response = True
- # Depending on where the message was destined to, add some implicit
- # commands. For example, if this was sent to the -join or -leave
- # addresses, it's the same as if 'join' or 'leave' commands were sent
- # to the -request address.
- is_address_command = False
- subaddress = msgdata.get('subaddress')
- if subaddress == 'join':
- self.command_lines.append('join')
- self.send_response = False
- is_address_command = True
- elif subaddress == 'leave':
- self.command_lines.append('leave')
- is_address_command = True
- self.send_response = False
- elif subaddress == 'confirm':
- mo = re.match(config.mta.verp_confirm_regexp, msg.get('to', ''))
- if mo:
- self.command_lines.append('confirm ' + mo.group('cookie'))
- is_address_command = True
- self.send_response = False
- # Stop processing if the address already contained a valid command
- if is_address_command:
- return
- # Extract the subject header and do RFC 2047 decoding.
- raw_subject = msg.get('subject', '')
- try:
- subject = str(make_header(decode_header(raw_subject)))
- # Mail commands must be ASCII.
- # NOTE(tommylikehu): remove all none ascii characters via encoding with ignore option.
- self.command_lines.append(subject.encode('us-ascii', 'ignore'))
- except (HeaderParseError, UnicodeError, LookupError):
- # The Subject header was unparseable or not ASCII. If the raw
- # subject is a unicode object, convert it to ASCII ignoring all
- # bogus characters. Otherwise, there's nothing in the subject
- # that we can use.
- if isinstance(raw_subject, str):
- safe_subject = raw_subject.encode('us-ascii', 'ignore')
- self.command_lines.append(safe_subject)
- # Find the first text/plain part of the message.
- part = None
- for part in typed_subpart_iterator(msg, 'text', 'plain'):
- break
- if part is None or part is not msg:
- # Either there was no text/plain part or we ignored some
- # non-text/plain parts.
- print(_('Ignoring non-text/plain MIME parts'), file=results)
- if part is None:
- # There was no text/plain part to be found.
- return
- body = part.get_payload(decode=True)
- # text/plain parts better have string payloads.
- assert body is not None, 'Non-string decoded payload'
- body = body.decode(part.get_content_charset('us-ascii'), errors='replace')
- lines = body.splitlines()
- # Use no more lines than specified
- max_lines = int(config.mailman.email_commands_max_lines)
- self.command_lines.extend(lines[:max_lines])
- self.ignored_lines.extend(lines[max_lines:])
- def __iter__(self):
- """Return each command line, split into space separated arguments."""
- while self.command_lines:
- line = self.command_lines.pop(0)
- self.processed_lines.append(line)
- parts = line.strip().split()
- if len(parts) == 0:
- continue
- # Ensure that all the parts are unicodes. Since we only accept
- # ASCII commands and arguments, ignore anything else.
- parts = [(part.lower()
- if isinstance(part, str)
- else part.decode('ascii', 'ignore').lower())
- for part in parts]
- yield parts
- @public
- @implementer(IEmailResults)
- class Results:
- """The email command results."""
- def __init__(self, charset='us-ascii'):
- self._output = StringIO()
- self.charset = charset
- print(_("""\
- The results of your email command are provided below.
- """), file=self._output)
- def write(self, text):
- if isinstance(text, bytes):
- text = text.decode(self.charset, 'ignore')
- self._output.write(text)
- def __str__(self):
- value = self._output.getvalue()
- assert isinstance(value, str), 'Not a string: %r' % value
- return value
- @public
- class CommandRunner(Runner):
- """The email command runner."""
- def _dispose(self, mlist, msg, msgdata):
- message_id = msg.get('message-id', 'n/a')
- # The policy here is similar to the Replybot policy. If a message has
- # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
- # the command message.
- precedence = msg.get('precedence', '').lower()
- ack = msg.get('x-ack', '').lower()
- if ack != 'yes' and precedence in ('bulk', 'junk', 'list'):
- log.info('%s Precedence: %s message discarded by: %s',
- message_id, precedence, mlist.request_address)
- return False
- # Do replybot for commands.
- replybot = config.handlers['replybot']
- replybot.process(mlist, msg, msgdata)
- if mlist.autorespond_requests == ResponseAction.respond_and_discard:
- # Respond and discard.
- log.info('%s -request message replied and discarded', message_id)
- return False
- # Now craft the response and process the command lines.
- charset = msg.get_param('charset')
- if charset is None:
- charset = 'us-ascii'
- results = Results(charset)
- # Include just a few key pieces of information from the original: the
- # sender, date, and message id.
- print(_('- Original message details:'), file=results)
- subject = msg.get('subject', 'n/a') # noqa: F841
- date = msg.get('date', 'n/a') # noqa: F841
- from_ = msg.get('from', 'n/a') # noqa: F841
- print(_(' From: $from_'), file=results)
- print(_(' Subject: $subject'), file=results)
- print(_(' Date: $date'), file=results)
- print(_(' Message-ID: $message_id'), file=results)
- print(_('\n- Results:'), file=results)
- finder = CommandFinder(msg, msgdata, results)
- for parts in finder:
- command = None
- # Try to find a command on this line. There may be a Re: prefix
- # (possibly internationalized) so try with the first and second
- # words on the line.
- if len(parts) > 0:
- command_name = parts.pop(0)
- command = config.commands.get(command_name)
- if command is None and len(parts) > 0:
- command_name = parts.pop(0)
- command = config.commands.get(command_name)
- if command is None:
- print(_('No such command: $command_name'), file=results)
- else:
- status = command.process(
- mlist, msg, msgdata, parts, results)
- assert status in ContinueProcessing, (
- 'Invalid status: %s' % status)
- if status == ContinueProcessing.no:
- break
- # All done. If we don't need to send response, return.
- if not finder.send_response:
- return
- # Strip blank lines and send the response.
- lines = [line.strip() for line in finder.command_lines if line]
- if len(lines) > 0:
- print(_('\n- Unprocessed:'), file=results)
- for line in lines:
- print(line, file=results)
- lines = [line.strip() for line in finder.ignored_lines if line]
- if len(lines) > 0:
- print(_('\n- Ignored:'), file=results)
- for line in lines:
- print(line, file=results)
- print(_('\n- Done.'), file=results)
- # Send a reply, but do not attach the original message. This is a
- # compromise because the original message is often helpful in tracking
- # down problems, but it's also a vector for backscatter spam.
- language = getUtility(ILanguageManager)[msgdata['lang']]
- reply = UserNotification(msg.sender, mlist.bounces_address,
- _('The results of your email commands'),
- lang=language)
- cte = msg.get('content-transfer-encoding')
- if cte is not None:
- reply['Content-Transfer-Encoding'] = cte
- # Find a charset for the response body. Try the original message's
- # charset first, then ascii, then latin-1 and finally falling back to
- # utf-8.
- reply_body = str(results)
- for charset in (results.charset, 'us-ascii', 'latin-1'):
- with suppress(UnicodeError):
- reply_body.encode(charset)
- break
- else:
- charset = 'utf-8'
- reply.set_payload(reply_body, charset=charset)
- reply.send(mlist)
-# configmap for mail exim4 service, these three files are directly read from exim config folder
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: mailman-exim4-configmap
- namespace: mail
-data:
- 25_mm3_macros: |
- # Place this file at
- # /etc/exim4/conf.d/main/25_mm3_macros
-
- domainlist mm3_domains=opengauss.org
- MM3_LMTP_HOST=mailman-core-0.mail-suit-service.mail.svc.cluster.local
- MM3_LMTP_PORT=8024
- # According to the configuration of: https://mailman.readthedocs.io/en/release-3.0/src/mailman/docs/MTA.html
- # We need updating this, for the purpose of delivering emails to the mailman
- MM3_HOME=/opt/mailman/var
-
- ################################################################
- # The configuration below is boilerplate:
- # you should not need to change it.
-
- # The path to the list receipt (used as the required file when
- # matching list addresses)
- MM3_LISTCHK=MM3_HOME/lists/${local_part}.${domain}
-
- 00_local_macros: |
- DKIM_CANON = relaxed
- DKIM_SELECTOR = default
- DKIM_DOMAIN = opengauss.org
- DKIM_FILE = /etc/exim4/dkim/opengauss.key
- DKIM_PRIVATE_KEY=${if exists{DKIM_FILE}{DKIM_FILE}{0}}
- MAIN_LOG_SELECTOR = +subject +deliver_time +received_sender +return_path_on_delivery +sender_on_delivery +unknown_in_list +smtp_protocol_error +smtp_syntax_error +tls_certificate_verified +tls_peerdn -host_lookup_failed
- MAIN_TLS_ENABLE = yes
- MAIN_TLS_CERTIFICATE = ${if exists{/etc/exim4/ssl_pool/${tls_sni}.crt}{/etc/exim4/ssl_pool/${tls_sni}.crt}{/etc/exim4/ssl_pool/opengauss.org.crt}}
- MAIN_TLS_PRIVATEKEY = ${if exists{/etc/exim4/ssl_pool/${tls_sni}.key}{/etc/exim4/ssl_pool//${tls_sni}.key}{/etc/exim4/ssl_pool/opengauss.org.key}}
- AUTH_SERVER_ALLOW_NOTLS_PASSWORDS = true
- daemon_smtp_ports = 25 : 465
- tls_on_connect_ports = 465
- MAIN_TLS_ADVERTISE_HOSTS=*
- IGNORE_SMTP_LINE_LENGTH_LIMIT=true
- MAIN_HARDCODE_PRIMARY_HOSTNAME = opengauss.org
-
- 55_mm3_transport: |
- # Place this file at
- # /etc/exim4/conf.d/transport/55_mm3_transport
-
- mailman3_transport:
- debug_print = "Email for mailman"
- driver = smtp
- protocol = lmtp
- allow_localhost
- hosts = MM3_LMTP_HOST
- port = MM3_LMTP_PORT
- rcpt_include_affixes = true
-
- 455_mm3_router: |
- # Place this file at
- # /etc/exim4/conf.d/router/455_mm3_router
-
- mailman3_router:
- driver = accept
- domains = +mm3_domains
- require_files = MM3_LISTCHK
- local_part_suffix_optional
- local_part_suffix = -admin : \
- -bounces : -bounces+* : \
- -confirm : -confirm+* : \
- -join : -leave : \
- -owner : -request : \
- -subscribe : -unsubscribe
- transport = mailman3_transport
-
- 01_exim4-config_listmacrosdefs: |
- # Place this file at
- # /etc/exim4/conf.d/main/01_exim4-config_listmacrosdefs
- log_file_path = /var/log/exim4/%s.%M
- exim_path = /usr/sbin/exim4
-
- # Macro defining the main configuration directory.
- # We do not use absolute paths.
- .ifndef CONFDIR
- CONFDIR = /etc/exim4
- .endif
-
- # debconf-driven macro definitions get inserted after this line
- UPEX4CmacrosUPEX4C = 1
-
- # Create domain and host lists for relay control
- # '@' refers to 'the name of the local host'
-
- # List of domains considered local for exim. Domains not listed here
- # need to be deliverable remotely.
- domainlist local_domains = MAIN_LOCAL_DOMAINS
-
- # List of recipient domains to relay _to_. Use this list if you're -
- # for example - fallback MX or mail gateway for domains.
- domainlist relay_to_domains = MAIN_RELAY_TO_DOMAINS
-
- # List of sender networks (IP addresses) to _unconditionally_ relay
- # _for_. If you intend to be SMTP AUTH server, you do not need to enter
- # anything here.
- hostlist relay_from_hosts = MAIN_RELAY_NETS
-
-
- # Decide which domain to use to add to all unqualified addresses.
- # If MAIN_PRIMARY_HOSTNAME_AS_QUALIFY_DOMAIN is defined, the primary
- # hostname is used. If not, but MAIN_QUALIFY_DOMAIN is set, the value
- # of MAIN_QUALIFY_DOMAIN is used. If both macros are not defined,
- # the first line of /etc/mailname is used.
- .ifndef MAIN_PRIMARY_HOSTNAME_AS_QUALIFY_DOMAIN
- .ifndef MAIN_QUALIFY_DOMAIN
- qualify_domain = ETC_MAILNAME
- .else
- qualify_domain = MAIN_QUALIFY_DOMAIN
- .endif
- .endif
-
- # listen on all all interfaces?
- .ifdef MAIN_LOCAL_INTERFACES
- local_interfaces = MAIN_LOCAL_INTERFACES
- .endif
-
- .ifndef LOCAL_DELIVERY
- # The default transport, set in /etc/exim4/update-exim4.conf.conf,
- # defaulting to mail_spool. See CONFDIR/conf.d/transport/ for possibilities
- LOCAL_DELIVERY=mail_spool
- .endif
-
- # The gecos field in /etc/passwd holds not only the name. see passwd(5).
- gecos_pattern = ^([^,:]*)
- gecos_name = $1
-
- .ifndef CHECK_RCPT_LOCAL_LOCALPARTS
- CHECK_RCPT_LOCAL_LOCALPARTS = ^[.] : ^.*[@%!/|`#&?]
- .endif
-
- .ifndef CHECK_RCPT_REMOTE_LOCALPARTS
- CHECK_RCPT_REMOTE_LOCALPARTS = ^[./|] : ^.*[@%!`#&?] : ^.*/\\.\\./
- .endif
-
- 30_exim4-config_examples: |
- # Place this file at
- # /etc/exim4/conf.d/auth/30_exim4-config_examples
-
- plain_server:
- driver = plaintext
- public_name = PLAIN
- server_condition = "${if crypteq{$auth3}{${extract{1}{:}{${lookup{$auth2}lsearch{CONFDIR/passwd}{$value}{*:*}}}}}{1}{0}}"
- server_set_id = $auth2
- server_prompts = :
- .ifndef AUTH_SERVER_ALLOW_NOTLS_PASSWORDS
- server_advertise_condition = ${if eq{$tls_in_cipher}{}{}{*}}
- .endif
-
- login_server:
- driver = plaintext
- public_name = LOGIN
- server_prompts = "Username:: : Password::"
- server_condition = "${if crypteq{$auth2}{${extract{1}{:}{${lookup{$auth1}lsearch{CONFDIR/passwd}{$value}{*:*}}}}}{1}{0}}"
- server_set_id = $auth1
- .ifndef AUTH_SERVER_ALLOW_NOTLS_PASSWORDS
- server_advertise_condition = ${if eq{$tls_in_cipher}{}{}{*}}
- .endif
-
- cram_md5:
- driver = cram_md5
- public_name = CRAM-MD5
- client_name = ${extract{1}{:}{${lookup{$host}nwildlsearch{CONFDIR/passwd.client}{$value}fail}}}
- client_secret = ${extract{2}{:}{${lookup{$host}nwildlsearch{CONFDIR/passwd.client}{$value}fail}}}
-
- # this returns the matching line from passwd.client and doubles all ^
- PASSWDLINE=${sg{\
- ${lookup{$host}nwildlsearch{CONFDIR/passwd.client}{$value}fail}\
- }\
- {\\N[\\^]\\N}\
- {^^}\
- }
-
- plain:
- driver = plaintext
- public_name = PLAIN
- .ifndef AUTH_CLIENT_ALLOW_NOTLS_PASSWORDS
- client_send = "<; ${if !eq{$tls_out_cipher}{}\
- {^${extract{1}{:}{PASSWDLINE}}\
- ^${sg{PASSWDLINE}{\\N([^:]+:)(.*)\\N}{\\$2}}\
- }fail}"
- .else
- client_send = "<; ^${extract{1}{:}{PASSWDLINE}}\
- ^${sg{PASSWDLINE}{\\N([^:]+:)(.*)\\N}{\\$2}}"
- .endif
-
- login:
- driver = plaintext
- public_name = LOGIN
- .ifndef AUTH_CLIENT_ALLOW_NOTLS_PASSWORDS
- # Return empty string if not non-TLS AND looking up $host in passwd-file
- # yields a non-empty string; fail otherwise.
- client_send = "<; ${if and{\
- {!eq{$tls_out_cipher}{}}\
- {!eq{PASSWDLINE}{}}\
- }\
- {}fail}\
- ; ${extract{1}{::}{PASSWDLINE}}\
- ; ${sg{PASSWDLINE}{\\N([^:]+:)(.*)\\N}{\\$2}}"
- .else
- # Return empty string if looking up $host in passwd-file yields a
- # non-empty string; fail otherwise.
- client_send = "<; ${if !eq{PASSWDLINE}{}\
- {}fail}\
- ; ${extract{1}{::}{PASSWDLINE}}\
- ; ${sg{PASSWDLINE}{\\N([^:]+:)(.*)\\N}{\\$2}}"
- .endif
-
- update-exim4-conf.conf: |
- dc_eximconfig_configtype='internet'
- dc_other_hostnames='opengauss.org;'
- dc_local_interfaces=''
- dc_readhost=''
- dc_relay_domains=''
- dc_minimaldns='false'
- dc_relay_nets='192.168.0.0/16'
- dc_smarthost=''
- CFILEMODE='644'
- dc_use_split_config='true'
- dc_hide_mailname=''
- dc_mailname_in_oh='true'
- dc_localdelivery='mail_spool'
-
-# configmap for mail web service
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: mailman-web-configmap
- namespace: mail
-data:
- base.html: |
- {% load i18n %}
- {% load staticfiles %}
- {% load gravatar %}
-
-
-
-
-
-
- {% block head_title %}{{ site_name }}{% endblock %}
-
-
-
-
- {% block additionalcss %}{% endblock %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% compress js %}
-
-
-
-
-
-
-
-
-
-
- {% endcompress %}
- {% block additionaljs %} {% endblock %}
-
- {% include 'hyperkitty/bottom.html' %}
-
-
-
- email.py: |
- # -*- coding: utf-8 -*-
- #
- # Copyright (C) 2014-2019 by the Free Software Foundation, Inc.
- #
- # This file is part of HyperKitty.
- #
- # HyperKitty 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.
- #
- # HyperKitty 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
- # HyperKitty. If not, see .
- #
- # Author: Aurelien Bompard
- #
-
- import os
- import re
- from email.message import EmailMessage
-
- from django.conf import settings
- from django.db import models, IntegrityError
- from django.utils.timezone import now, get_fixed_timezone
-
- from hyperkitty.lib.analysis import compute_thread_order_and_depth
- from .common import VotesCachedValue
- from .mailinglist import MailingList
- from .thread import Thread
- from .vote import Vote
-
- import logging
- logger = logging.getLogger(__name__)
-
-
- class Email(models.Model):
- """
- An archived email, from a mailing-list. It is identified by both the list
- name and the message id.
- """
- mailinglist = models.ForeignKey(
- "MailingList", related_name="emails", on_delete=models.CASCADE)
- message_id = models.CharField(max_length=255, db_index=True)
- message_id_hash = models.CharField(max_length=255, db_index=True)
- sender = models.ForeignKey(
- "Sender", related_name="emails", on_delete=models.CASCADE)
- sender_name = models.CharField(max_length=255, null=True, blank=True)
- subject = models.CharField(max_length=512, db_index=True)
- content = models.TextField()
- date = models.DateTimeField(db_index=True)
- timezone = models.SmallIntegerField()
- in_reply_to = models.CharField(
- max_length=255, null=True, blank=True, db_index=True)
- # Delete behavior is handled by on_pre_delete()
- parent = models.ForeignKey(
- "self", blank=True, null=True, on_delete=models.DO_NOTHING,
- related_name="children")
- thread = models.ForeignKey(
- "Thread", related_name="emails", on_delete=models.CASCADE)
- archived_date = models.DateTimeField(default=now, db_index=True)
- thread_depth = models.IntegerField(default=0)
- thread_order = models.IntegerField(null=True, blank=True, db_index=True)
-
- ADDRESS_REPLACE_RE = re.compile(r"([\w.+-]+)@([\w.+-]+)")
-
- def __init__(self, *args, **kwargs):
- super(Email, self).__init__(*args, **kwargs)
- self.cached_values = {
- "votes": VotesCachedValue(self),
- }
-
- def __lt__(self, other):
- return self.date < other.date
-
- class Meta:
- unique_together = ("mailinglist", "message_id")
-
- def get_votes(self):
- return self.cached_values["votes"]()
-
- def vote(self, value, user):
- # Checks if the user has already voted for this message.
- existing = self.votes.filter(user=user).first()
- if existing is not None and existing.value == value:
- return # Vote already recorded (should I raise an exception?)
- if value not in (0, 1, -1):
- raise ValueError("A vote can only be +1 or -1 (or 0 to cancel)")
- if existing is not None:
- # vote changed or cancelled
- if value == 0:
- existing.delete()
- else:
- existing.value = value
- existing.save()
- else:
- # new vote
- vote = Vote(email=self, user=user, value=value)
- vote.save()
-
- def set_parent(self, parent):
- if self.id == parent.id:
- raise ValueError("An email can't be its own parent")
- # Compute the subthread
- subthread = [self]
-
- def _collect_children(current_email):
- children = list(current_email.children.all())
- if not children:
- return
- subthread.extend(children)
- for child in children:
- _collect_children(child)
- _collect_children(self)
- # now set my new parent value
- old_parent_id = self.parent_id
- self.parent = parent
- self.save(update_fields=["parent_id"])
- # If my future parent is in my current subthread, I need to set its
- # parent to my current parent
- if parent in subthread:
- parent.parent_id = old_parent_id
- parent.save(update_fields=["parent_id"])
- # do it after setting the new parent_id to avoid having two
- # parent_ids set to None at the same time (IntegrityError)
- if self.thread_id != parent.thread_id:
- # we changed the thread, reattach the subthread
- former_thread = self.thread
- for child in subthread:
- child.thread = parent.thread
- child.save(update_fields=["thread_id"])
- if child.date > parent.thread.date_active:
- parent.thread.date_active = child.date
- parent.thread.save()
- # if we were the starting email, or former thread may be empty
- if former_thread.emails.count() == 0:
- former_thread.delete()
- compute_thread_order_and_depth(parent.thread)
-
- def as_message(self, escape_addresses=True):
- # http://wordeology.com/computer/how-to-send-good-unicode-email-with-python.html
- # http://stackoverflow.com/questions/31714221/how-to-send-an-email-with-quoted
- # http://stackoverflow.com/questions/9403265/how-do-i-use-python/9509718#9509718
- msg = EmailMessage()
-
- # Headers
- unixfrom = "From %s %s" % (
- self.sender.address, self.archived_date.strftime("%c"))
- assert isinstance(self.sender.address, str)
- header_from = self.sender.address
- if self.sender_name and self.sender_name != self.sender.address:
- header_from = "%s <%s>" % (self.sender_name, header_from)
- header_to = self.mailinglist.name
- msg.set_unixfrom(unixfrom)
- headers = (
- ("From", header_from),
- ("To", header_to),
- ("Subject", self.subject),
- )
- for header_name, header_value in headers:
- msg[header_name] = header_value
- tz = get_fixed_timezone(self.timezone)
- header_date = self.date.astimezone(tz).replace(microsecond=0)
- # Date format: http://tools.ietf.org/html/rfc5322#section-3.3
- msg["Date"] = header_date.strftime("%a, %d %b %Y %H:%M:%S %z")
- msg["Message-ID"] = "<%s>" % self.message_id
- if self.in_reply_to:
- msg["In-Reply-To"] = self.in_reply_to
-
- # Body
- content = self.ADDRESS_REPLACE_RE.sub(r"\1(a)\2", self.content)
-
- # Enforce `multipart/mixed` even when there are no attachments
- # Q: Why are all emails supposed to be multipart?
- msg.set_content(content, subtype='plain')
- msg.make_mixed()
-
- # Attachments
- for attachment in self.attachments.order_by("counter"):
- mimetype = attachment.content_type.split('/', 1)
- msg.add_attachment(attachment.get_content(), maintype=mimetype[0],
- subtype=mimetype[1], filename=attachment.name)
-
- return msg
-
- @property
- def display_fixed(self):
- return "@@" in self.content
-
- def _set_message_id_hash(self):
- from hyperkitty.lib.utils import get_message_id_hash # circular import
- if not self.message_id_hash:
- self.message_id_hash = get_message_id_hash(self.message_id)
-
- def on_post_init(self):
- self._set_message_id_hash()
-
- def on_post_created(self):
- self.thread.on_email_added(self)
- self.mailinglist.on_email_added(self)
- if not getattr(settings, "HYPERKITTY_BATCH_MODE", False):
- # For batch imports, let the cron job do the work
- from hyperkitty.tasks import check_orphans
- check_orphans.delay(self.id)
-
- def on_pre_save(self):
- self._set_message_id_hash()
- # Link to the thread
- if self.thread_id is None:
- # Create the thread if not found
- thread, _thread_created = Thread.objects.get_or_create(
- mailinglist=self.mailinglist,
- thread_id=self.message_id_hash)
- self.thread = thread
- # Make sure there is only one email with parent_id == None in a thread
- if self.parent_id is not None:
- return
- starters = Email.objects.filter(
- thread=self.thread, parent_id__isnull=True
- ).values_list("id", flat=True)
- if len(starters) > 0 and list(starters) != [self.id]:
- raise IntegrityError("There can be only one email with "
- "parent_id==None in the same thread")
-
- def on_post_save(self):
- pass
-
- def on_pre_delete(self):
- # Reset parent_id
- children = self.children.order_by("date")
- if not children:
- return
- if self.parent is None:
- # Temporarily set the email's parent_id to not None, to allow the
- # next email to be the starting email (there's a check on_save for
- # duplicate thread starters)
- self.parent = self
- self.save(update_fields=["parent"])
- starter = children[0]
- starter.parent = None
- starter.save(update_fields=["parent"])
- children.all().update(parent=starter)
- else:
- children.update(parent=self.parent)
-
- def on_post_delete(self):
- try:
- thread = Thread.objects.get(id=self.thread_id)
- except Thread.DoesNotExist:
- pass
- else:
- thread.on_email_deleted(self)
- try:
- mlist = MailingList.objects.get(pk=self.mailinglist_id)
- except MailingList.DoesNotExist:
- pass
- else:
- mlist.on_email_deleted(self)
-
- def on_vote_added(self, vote):
- from hyperkitty.tasks import rebuild_email_cache_votes
- rebuild_email_cache_votes.delay(self.id)
-
- on_vote_deleted = on_vote_added
-
-
- class Attachment(models.Model):
- email = models.ForeignKey(
- "Email", related_name="attachments", on_delete=models.CASCADE)
- counter = models.SmallIntegerField()
- name = models.CharField(max_length=255)
- content_type = models.CharField(max_length=255)
- encoding = models.CharField(max_length=255, null=True)
- size = models.IntegerField(null=True)
- content = models.BinaryField(null=True)
-
- class Meta:
- unique_together = ("email", "counter")
-
- def on_pre_save(self):
- # set the size
- if not self.size and self.content is not None:
- self.size = len(self.content)
-
- def _get_folder(self):
- global_folder = getattr(
- settings, "HYPERKITTY_ATTACHMENT_FOLDER", None)
- if global_folder is None:
- return None
- mlist = self.email.mailinglist.name
- try:
- listname, domain = mlist.rsplit("@", 1)
- except ValueError:
- listname = "none"
- domain = mlist
- return os.path.join(
- global_folder, domain, listname,
- self.email.message_id_hash[0:2],
- self.email.message_id_hash[2:4],
- self.email.message_id_hash[4:6],
- str(self.email.id),
- )
-
- def get_content(self):
- folder = self._get_folder()
- if folder is None:
- return bytes(self.content)
- filepath = os.path.join(folder, str(self.counter))
- if not os.path.exists(filepath):
- logger.error("Could not find local attachment %s for email %s",
- self.counter, self.email.id)
- return ""
- with open(filepath, "rb") as f:
- content = f.read()
- return content
-
- def set_content(self, content):
- if isinstance(content, str):
- if self.encoding is not None:
- content = content.encode(self.encoding)
- else:
- content = content.encode('utf-8')
- self.size = len(content)
- folder = self._get_folder()
- if folder is None:
- self.content = content
- return
- if not os.path.exists(folder):
- os.makedirs(folder)
- with open(os.path.join(folder, str(self.counter)), "wb") as f:
- f.write(content)
- self.content = None
- settings_local.py: |
- import os
- import socket
- import ipaddress
-
- DEBUG = False
-
- EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
- #NOTE: this is the MTA host, we need to update it.
- EMAIL_HOST = 'mailman-exim4-service.mail.svc.cluster.local'
- EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
- EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
- EMAIL_PORT = 25
-
- mailman_ip_address = socket.gethostbyname(os.environ.get('MAILMAN_HOST_IP')).split('.')
- mailman_ip_cidr = "{0}.{1}.0.0/16".format(mailman_ip_address[0], mailman_ip_address[1])
- MAILMAN_ARCHIVER_FROM = [str(ip) for ip in ipaddress.IPv4Network(mailman_ip_cidr)]
- MAILMAN_NODE_IP_CIDR = "192.168.0.0/16"
- MAILMAN_NODE_IP_FROM = [str(ip) for ip in ipaddress.IPv4Network(MAILMAN_NODE_IP_CIDR)]
- MAILMAN_ARCHIVER_FROM.extend(MAILMAN_NODE_IP_FROM)
-
- SERVICE_IP = socket.gethostbyname("mailweb.opengauss.org")
-
- ALLOWED_HOSTS = [
- "localhost", # Archiving API from Mailman, keep it.
- # Add here all production URLs you may have.
- "192.168.0.164",
- "mailman-core-0.mail-suit-service.mail.svc.cluster.local",
- "mailman-web-0.mail-suit-service.mail.svc.cluster.local",
- #NOTE: This is the public ip address of the served host
- "opengauss.org",
- #NOTE: this can be removed if domain name finally get used.
- "49.0.246.28",
- "119.8.40.18",
- "mailweb.opengauss.org",
- "mail.opengauss.org",
- "mailman-web",
- "mailman-web-service.mail.svc.cluster.local:8000",
- SERVICE_IP,
- os.environ.get('SERVE_FROM_DOMAIN'),
- os.environ.get('DJANGO_ALLOWED_HOSTS'),
- ]
-
- COMPRESS_CSS_HASHING_METHOD = 'content'
- INSTALLED_APPS = [
- 'hyperkitty',
- 'postorius',
- 'django_mailman3',
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'rest_framework',
- 'django_gravatar',
- 'compressor',
- 'haystack',
- 'django_extensions',
- 'django_q',
- 'allauth',
- 'allauth.account',
- 'allauth.socialaccount',
- ]
- default.conf: |
- server {
- listen 443 ssl;
-
- root /opt/mailman-web-data;
- index index.html;
- server_name mailweb.opengauss.org;
- ssl_certificate /etc/nginx/ssl/server.crt;
- ssl_certificate_key /etc/nginx/ssl/server.key;
- server_tokens off;
- location /static {
- alias /opt/mailman-web-data/static;
- }
- location / {
- uwsgi_pass 127.0.0.1:8080;
- include uwsgi_params;
- uwsgi_read_timeout 300;
- }
- location ^~ /admin {
- deny all;
- }
- location /accounts/signup {
- rewrite ^/accounts/signup(.*)$ /postorius/lists/;
- }
- }
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: mailman-webpage-hack
- namespace: mail
-data:
- list_forms.py: |
- # -*- coding: utf-8 -*-
- # Copyright (C) 2017-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 .
- #
-
- import os
- import re
- from django import forms
- from django.core.validators import validate_email
- from django.core.exceptions import ValidationError
- from django.utils.safestring import mark_safe
- from django.utils.translation import ugettext_lazy as _
- from django_mailman3.lib.mailman import get_mailman_client
-
- from postorius.forms.fields import ListOfStringsField
-
-
- ACTION_CHOICES = (
- ("hold", _("Hold for moderation")),
- ("reject", _("Reject (with notification)")),
- ("discard", _("Discard (no notification)")),
- ("accept", _("Accept immediately (bypass other rules)")),
- ("defer", _("Default processing")),
- )
-
-
- EMPTY_STRING = ''
-
-
- class ListNew(forms.Form):
-
- """
- Form fields to add a new list. Languages are hard coded which should
- be replaced by a REST lookup of available languages.
- """
- listname = forms.CharField(
- label=_('List Name'),
- required=True,
- error_messages={'required': _('Please enter a name for your list.'),
- 'invalid': _('Please enter a valid list name.')})
- mail_host = forms.ChoiceField()
- list_owner = forms.EmailField(
- label=_('Initial list owner address'),
- error_messages={
- 'required': _("Please enter the list owner's email address.")},
- required=True)
- advertised = forms.ChoiceField(
- widget=forms.RadioSelect(),
- label=_('Advertise this list?'),
- error_messages={
- 'required': _("Please choose a list type.")},
- required=True,
- choices=(
- (True, _("Advertise this list in list index")),
- (False, _("Hide this list in list index"))))
- list_style = forms.ChoiceField()
- description = forms.CharField(
- label=_('Description'),
- required=False)
-
- def __init__(self, domain_choices, style_choices, *args, **kwargs):
- super(ListNew, self).__init__(*args, **kwargs)
- self.fields["mail_host"] = forms.ChoiceField(
- widget=forms.Select(),
- label=_('Mail Host'),
- required=True,
- choices=domain_choices,
- error_messages={'required': _("Choose an existing Domain."),
- 'invalid': _("Choose a valid Mail Host")})
- self.fields["list_style"] = forms.ChoiceField(
- widget=forms.Select(),
- label=_('List Style'),
- required=True,
- choices=style_choices,
- error_messages={'required': _("Choose a List Style."),
- 'invalid': _("Choose a valid List Style.")})
- if len(domain_choices) < 2:
- self.fields["mail_host"].help_text = _(
- "Site admin has not created any domains")
- # if len(choices) < 2:
- # help_text=_("No domains available: " +
- # "The site admin must create new domains " +
- # "before you will be able to create a list")
-
- def clean_listname(self):
- try:
- validate_email(self.cleaned_data['listname'] + '@example.net')
- except ValidationError:
- # TODO (maxking): Error should atleast point to what is a valid
- # listname. It may not always be obvious which characters aren't
- # allowed in a listname.
- raise forms.ValidationError(_("Please enter a valid listname"))
- return self.cleaned_data['listname']
-
- class Meta:
-
- """
- Class to handle the automatic insertion of fieldsets and divs.
-
- To use it: add a list for each wished fieldset. The first item in
- the list should be the wished name of the fieldset, the following
- the fields that should be included in the fieldset.
- """
- layout = [["List Details",
- "listname",
- "mail_host",
- "list_style",
- "list_owner",
- "description",
- "advertised"], ]
-
-
- class ListSubscribe(forms.Form):
- """Form fields to join an existing list.
- """
-
- email = forms.ChoiceField(
- label=_('Your email address'),
- validators=[validate_email],
- widget=forms.Select(),
- error_messages={
- 'required': _('Please enter an email address.'),
- 'invalid': _('Please enter a valid email address.')})
-
- display_name = forms.CharField(
- label=_('Your name (optional)'), required=False)
-
- def __init__(self, user_emails, *args, **kwargs):
- super(ListSubscribe, self).__init__(*args, **kwargs)
- self.fields['email'].choices = ((address, address)
- for address in user_emails)
-
-
- class ListAnonymousSubscribe(forms.Form):
- """Form fields to join an existing list as an anonymous user.
- """
-
- email = forms.CharField(
- label=_('Your email address'),
- validators=[validate_email],
- error_messages={
- 'required': _('Please enter an email address.'),
- 'invalid': _('Please enter a valid email address.')})
-
- display_name = forms.CharField(
- label=_('Your name (optional)'), required=False)
-
- privacy_check = forms.BooleanField(
- initial=False,
- required=True,
- label=_(mark_safe(
- 'I have read and agree to the community Privacy Statement and '
- 'Community Conduct.' % (os.getenv("PRIVACY_LINK", "#"), os.getenv("CONDUCT_LINK"))
- )),
- widget=forms.CheckboxInput())
-
-
- class ListSettingsForm(forms.Form):
- """
- Base class for list settings forms.
- """
- mlist_properties = []
-
- def __init__(self, *args, **kwargs):
- self._mlist = kwargs.pop('mlist')
- super(ListSettingsForm, self).__init__(*args, **kwargs)
-
-
- SUBSCRIPTION_POLICY_CHOICES = (
- ('open', _('Open')),
- ('confirm', _('Confirm')),
- ('moderate', _('Moderate')),
- ('confirm_then_moderate', _('Confirm, then moderate')),
- )
-
-
- class ListSubscriptionPolicyForm(ListSettingsForm):
- """
- List subscription policy settings.
- """
- subscription_policy = forms.ChoiceField(
- label=_('Subscription Policy'),
- choices=SUBSCRIPTION_POLICY_CHOICES,
- help_text=_('Open: Subscriptions are added automatically\n'
- 'Confirm: Subscribers need to confirm the subscription '
- 'using an email sent to them\n'
- 'Moderate: Moderators will have to authorize '
- 'each subscription manually.\n'
- 'Confirm then Moderate: First subscribers have to confirm,'
- ' then a moderator '
- 'needs to authorize.'))
-
-
- class ArchiveSettingsForm(ListSettingsForm):
- """
- Set the general archive policy.
- """
- mlist_properties = ['archivers']
-
- archive_policy_choices = (
- ("public", _("Public archives")),
- ("private", _("Private archives")),
- ("never", _("Do not archive this list")),
- )
-
- archive_policy = forms.ChoiceField(
- choices=archive_policy_choices,
- widget=forms.RadioSelect,
- label=_('Archive policy'),
- help_text=_('Policy for archiving messages for this list'),
- )
-
- archivers = forms.MultipleChoiceField(
- widget=forms.CheckboxSelectMultiple,
- label=_('Active archivers'),
- required=False) # May be empty if no archivers are desired.
-
- def __init__(self, *args, **kwargs):
- super(ArchiveSettingsForm, self).__init__(*args, **kwargs)
- archiver_opts = sorted(self._mlist.archivers.keys())
- self.fields['archivers'].choices = sorted(
- [(key, key) for key in archiver_opts])
- if self.initial:
- self.initial['archivers'] = [
- key for key in archiver_opts if self._mlist.archivers[key] is True] # noqa
-
- def clean_archivers(self):
- result = {}
- for archiver, etc in self.fields['archivers'].choices:
- result[archiver] = archiver in self.cleaned_data['archivers']
- self.cleaned_data['archivers'] = result
- return result
-
-
- class MessageAcceptanceForm(ListSettingsForm):
- """
- List messages acceptance settings.
- """
- acceptable_aliases = ListOfStringsField(
- label=_("Acceptable aliases"),
- required=False,
- help_text=_(
- 'This is a list, one per line, of addresses and regexps matching '
- 'addresses that are acceptable in To: or Cc: in lieu of the list '
- 'posting address when `require_explicit_destination\' is enabled. '
- ' Entries are either email addresses or regexps matching email '
- 'addresses. Regexps are entries beginning with `^\' and are '
- 'matched against every recipient address in the message. The '
- 'matching is performed with Python\'s re.match() function, meaning'
- ' they are anchored to the start of the string.'))
- require_explicit_destination = forms.BooleanField(
- widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
- required=False,
- label=_('Require Explicit Destination'),
- help_text=_(
- 'This checks to ensure that the list posting address or an '
- 'acceptable alias explicitly appears in a To: or Cc: header in '
- 'the post.'))
- administrivia = forms.BooleanField(
- widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
- required=False,
- label=_('Administrivia'),
- help_text=_(
- 'Administrivia tests will check postings to see whether it\'s '
- 'really meant as an administrative request (like subscribe, '
- 'unsubscribe, etc), and will add it to the the administrative '
- 'requests queue, notifying the administrator of the new request, '
- 'in the process.'))
- default_member_action = forms.ChoiceField(
- widget=forms.RadioSelect(),
- label=_('Default action to take when a member posts to the list'),
- error_messages={
- 'required': _("Please choose a default member action.")},
- required=True,
- choices=ACTION_CHOICES,
- help_text=_(
- 'Default action to take when a member posts to the list.\n'
- 'Hold: This holds the message for approval by the list '
- 'moderators.\n'
- 'Reject: this automatically rejects the message by sending a '
- 'bounce notice to the post\'s author. The text of the bounce '
- 'notice can be configured by you.\n'
- 'Discard: this simply discards the message, with no notice '
- 'sent to the post\'s author.\n'
- 'Accept: accepts any postings without any further checks.\n'
- 'Default Processing: run additional checks and accept '
- 'the message.'))
- default_nonmember_action = forms.ChoiceField(
- widget=forms.RadioSelect(),
- label=_('Default action to take when a non-member posts to the '
- 'list'),
- error_messages={
- 'required': _("Please choose a default non-member action.")},
- required=True,
- choices=ACTION_CHOICES,
- help_text=_(
- 'When a post from a non-member is received, the message\'s sender '
- 'is matched against the list of explicitly accepted, held, '
- 'rejected (bounced), and discarded addresses. '
- 'If no match is found, then this action is taken.'))
- max_message_size = forms.IntegerField(
- min_value=0,
- label=_('Maximum message size'),
- required=False,
- help_text=_(
- 'The maximum allowed message size. '
- 'This can be used to prevent emails with large attachments. '
- 'A size of 0 disables the check.'))
- max_num_recipients = forms.IntegerField(
- min_value=0,
- label=_('Maximum number of recipients'),
- required=False,
- help_text=_(
- 'The maximum number of recipients for a message. '
- 'This can be used to prevent mass mailings from being accepted. '
- 'A value of 0 disables the check.'))
-
- def clean_acceptable_aliases(self):
- # python's urlencode will drop this attribute completely if an empty
- # list is passed with doseq=True. To make it work for us, we instead
- # use an empty string to signify an empty value. In turn, Core will
- # also consider an empty value to be empty list for list-of-strings
- # field.
- if not self.cleaned_data['acceptable_aliases']:
- return EMPTY_STRING
- for alias in self.cleaned_data['acceptable_aliases']:
- if alias.startswith('^'):
- try:
- re.compile(alias)
- except re.error as e:
- raise forms.ValidationError(
- _('Invalid alias regexp: {}: {}').format(alias, e.msg))
- else:
- try:
- validate_email(alias)
- except ValidationError:
- raise forms.ValidationError(
- _('Invalid alias email: {}').format(alias))
- return self.cleaned_data['acceptable_aliases']
-
-
- class DigestSettingsForm(ListSettingsForm):
- """
- List digest settings.
- """
- digest_size_threshold = forms.DecimalField(
- label=_('Digest size threshold'),
- help_text=_('How big in Kb should a digest be before '
- 'it gets sent out?'))
-
-
- class DMARCMitigationsForm(ListSettingsForm):
- """
- DMARC Mitigations list settings.
- """
- dmarc_mitigate_action = forms.ChoiceField(
- label=_('DMARC mitigation action'),
- widget=forms.Select(),
- required=False,
- error_messages={
- 'required': _("Please choose a DMARC mitigation action.")},
- choices=(
- ('no_mitigation', _('No DMARC mitigations')),
- ('munge_from', _('Replace From: with list address')),
- ('wrap_message',
- _('Wrap the message in an outer message From: the list.')),
- ('reject', _('Reject the message')),
- ('discard', _('Discard the message'))),
- help_text=_(
- 'The action to apply to messages From: a domain publishing a '
- 'DMARC policy of reject or quarantine or to all messages if '
- 'DMARC Mitigate unconditionally is True.'))
- dmarc_mitigate_unconditionally = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('DMARC Mitigate unconditionally'),
- help_text=_(
- 'If DMARC mitigation action is munge_from or wrap_message, '
- 'should it apply to all messages regardless of the DMARC policy '
- 'of the From: domain.'))
- dmarc_moderation_notice = forms.CharField(
- label=_('DMARC rejection notice'),
- required=False,
- widget=forms.Textarea(),
- help_text=_(
- 'Text to replace the default reason in any rejection notice to '
- 'be sent when DMARC mitigation action of reject applies.'))
- dmarc_wrapped_message_text = forms.CharField(
- label=_('DMARC wrapped message text'),
- required=False,
- widget=forms.Textarea(),
- help_text=_(
- 'Text to be added as a separate text/plain MIME part preceding '
- 'the original message part in the wrapped message when DMARC '
- 'mitigation action of wrap message applies.'))
-
-
- class AlterMessagesForm(ListSettingsForm):
- """
- Alter messages list settings.
- """
- filter_content = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Filter content'),
- help_text=_('Should Mailman filter the content of list traffic '
- 'according to the settings below?'))
- collapse_alternatives = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Collapse alternatives'),
- help_text=_('Should Mailman collapse multipart/alternative to '
- 'its first part content?'))
- convert_html_to_plaintext = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Convert html to plaintext'),
- help_text=_('Should Mailman convert text/html parts to plain text? '
- 'This conversion happens after MIME attachments '
- 'have been stripped.'))
- anonymous_list = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Anonymous list'),
- help_text=_('Hide the sender of a message, '
- 'replacing it with the list address '
- '(Removes From, Sender and Reply-To fields)'))
- include_rfc2369_headers = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Include RFC2369 headers'),
- help_text=_(
- 'Yes is highly recommended. RFC 2369 defines a set of List-* '
- 'headers that are normally added to every message sent to the '
- 'list membership. These greatly aid end-users who are using '
- 'standards compliant mail readers. They should normally always '
- 'be enabled. However, not all mail readers are standards '
- 'compliant yet, and if you have a large number of members who are '
- 'using non-compliant mail readers, they may be annoyed at these '
- 'headers. You should first try to educate your members as to why '
- 'these headers exist, and how to hide them in their mail clients. '
- 'As a last resort you can disable these headers, but this is not '
- 'recommended (and in fact, your ability to disable these headers '
- 'may eventually go away).'))
- allow_list_posts = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_("Include the list post header"),
- help_text=_(
- "This can be set to no for announce lists that do not wish to "
- "include the List-Post header because posting to the list is "
- "discouraged."))
- reply_to_address = forms.CharField(
- label=_('Explicit reply-to address'),
- required=False,
- help_text=_(
- 'This option allows admins to set an explicit Reply-to address. '
- 'It is only used if the reply-to is set to use an explicitly set '
- 'header'))
- first_strip_reply_to = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- help_text=_(
- 'Should any existing Reply-To: header found in the original '
- 'message be stripped? If so, this will be done regardless of '
- 'whether an explict Reply-To: header is added by Mailman or not.'))
- reply_goes_to_list = forms.ChoiceField(
- label=_('Reply goes to list'),
- widget=forms.Select(),
- required=False,
- error_messages={
- 'required': _("Please choose a reply-to action.")},
- choices=(
- ('no_munging', _('No Munging')),
- ('point_to_list', _('Reply goes to list')),
- ('explicit_header', _('Explicit Reply-to header set')),
- ('explicit_header_only', _('Explicit Reply-to set; no Cc added'))),
- help_text=_(
- 'Where are replies to list messages directed? No Munging is '
- 'strongly recommended for most mailing lists. \nThis option '
- 'controls what Mailman does to the Reply-To: header in messages '
- 'flowing through this mailing list. When set to No Munging, no '
- 'Reply-To: header is '
- 'added by Mailman, although if one is present in the original '
- 'message, it is not stripped. Setting this value to either Reply '
- 'to List, Explicit Reply, or Reply Only causes Mailman to insert '
- 'a specific Reply-To: header in all messages, overriding the '
- 'header in the original message if necessary '
- '(Explicit Reply inserts the value of reply_to_address). '
- 'Explicit Reply-to set; no Cc added is useful for'
- 'announce-only lists where you want to avoid someone replying '
- 'to the list address. There are many reasons not to introduce or '
- 'override the Reply-To: header. One is that some posters depend '
- 'on their own Reply-To: settings to convey their valid return '
- 'address. Another is that modifying Reply-To: makes it much more '
- 'difficult to send private replies. See `Reply-To\' Munging '
- 'Considered Harmful for a general discussion of this issue. '
- 'See Reply-To Munging Considered Useful for a dissenting opinion. '
- 'Some mailing lists have restricted '
- 'posting privileges, with a parallel list devoted to discussions. '
- 'Examples are `patches\' or `checkin\' lists, where software '
- 'changes are posted by a revision control system, but discussion '
- 'about the changes occurs on a developers mailing list. To '
- 'support these types of mailing lists, select Explicit Reply and '
- 'set the Reply-To: address option to point to the parallel list.'))
- posting_pipeline = forms.ChoiceField(
- label=_('Pipeline'),
- widget=forms.Select(),
- required=False,
- choices=lambda: ((p, p) for p in get_mailman_client()
- .pipelines['pipelines']),
- help_text=_('Type of pipeline you want to use for this mailing list'))
-
-
- class ListAutomaticResponsesForm(ListSettingsForm):
- """
- List settings for automatic responses.
- """
- autorespond_choices = (
- ("respond_and_continue", _("Respond and continue processing")),
- ("respond_and_discard", _("Respond and discard message")),
- ("none", _("No automatic response")))
- autorespond_owner = forms.ChoiceField(
- choices=autorespond_choices,
- widget=forms.RadioSelect,
- label=_('Autorespond to list owner'),
- help_text=_('Should Mailman send an auto-response to '
- 'emails sent to the -owner address?'))
- autoresponse_owner_text = forms.CharField(
- label=_('Autoresponse owner text'),
- widget=forms.Textarea(),
- required=False,
- help_text=_('Auto-response text to send to -owner emails.'))
- autorespond_postings = forms.ChoiceField(
- choices=autorespond_choices,
- widget=forms.RadioSelect,
- label=_('Autorespond postings'),
- help_text=_('Should Mailman send an auto-response to '
- 'mailing list posters?'))
- autoresponse_postings_text = forms.CharField(
- label=_('Autoresponse postings text'),
- widget=forms.Textarea(),
- required=False,
- help_text=_('Auto-response text to send to mailing list posters.'))
- autorespond_requests = forms.ChoiceField(
- choices=autorespond_choices,
- widget=forms.RadioSelect,
- label=_('Autorespond requests'),
- help_text=_(
- 'Should Mailman send an auto-response to emails sent to the '
- '-request address? If you choose yes, decide whether you want '
- 'Mailman to discard the original email, or forward it on to the '
- 'system as a normal mail command.'))
- autoresponse_request_text = forms.CharField(
- label=_('Autoresponse request text'),
- widget=forms.Textarea(),
- required=False,
- help_text=_('Auto-response text to send to -request emails.'))
- autoresponse_grace_period = forms.CharField(
- label=_('Autoresponse grace period'),
- help_text=_(
- 'Number of days between auto-responses to either the mailing list '
- 'or -request/-owner address from the same poster. Set to zero '
- '(or negative) for no grace period (i.e. auto-respond to every '
- 'message).'))
- respond_to_post_requests = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Notify users of held messages'),
- help_text=_(
- 'Should Mailman notify users about their messages held for '
- 'approval. If you say \'No\', no notifications will be sent '
- 'to users about the pending approval on their messages.'))
- send_welcome_message = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- required=False,
- label=_('Send welcome message'),
- help_text=_(
- 'Send welcome message to newly subscribed members? '
- 'Turn this off only if you plan on subscribing people manually '
- 'and don\'t want them to know that you did so. This option is '
- 'most useful for transparently migrating lists from some other '
- 'mailing list manager to Mailman.'))
- welcome_message_uri = forms.CharField(
- label=_('URI for the welcome message'),
- required=False,
- help_text=_(
- 'If a welcome message is to be sent to subscribers, you can '
- 'specify a URI that gives the text of this message.'),
- )
- goodbye_message_uri = forms.CharField(
- label=_('URI for the good bye message'),
- required=False,
- help_text=_(
- 'If a good bye message is to be sent to unsubscribers, you can '
- 'specify a URI that gives the text of this message.'),
- )
- admin_immed_notify = forms.BooleanField(
- widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
- required=False,
- label=_('Admin immed notify'),
- help_text=_(
- 'Should the list moderators get immediate notice of new requests, '
- 'as well as daily notices about collected ones? List moderators '
- '(and list administrators) are sent daily reminders of requests '
- 'pending approval, like subscriptions to a moderated list, '
- 'or postings that are being held for one reason or another. '
- 'Setting this option causes notices to be sent immediately on the '
- 'arrival of new requests as well. '))
- admin_notify_mchanges = forms.BooleanField(
- widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
- required=False,
- label=_('Notify admin of membership changes'),
- help_text=_('Should administrator get notices of '
- 'subscribes and unsubscribes?'))
-
-
- class ListIdentityForm(ListSettingsForm):
- """
- List identity settings.
- """
- advertised = forms.TypedChoiceField(
- choices=((True, _('Yes')), (False, _('No'))),
- widget=forms.RadioSelect,
- label=_('Show list on index page'),
- help_text=_('Choose whether to include this list '
- 'on the list of all lists'))
- description = forms.CharField(
- label=_('Description'),
- required=False,
- help_text=_(
- 'This description is used when the mailing list is listed with '
- 'other mailing lists, or in headers, and so forth. It should be '
- 'as succinct as you can get it, while still identifying what the '
- 'list is.'),
- )
- info = forms.CharField(
- label=_('Information'),
- help_text=_('A longer description of this mailing list.'),
- required=False,
- widget=forms.Textarea())
- display_name = forms.CharField(
- label=_('Display name'),
- required=False,
- help_text=_('Display name is the name shown in the web interface.')
- )
- subject_prefix = forms.CharField(
- label=_('Subject prefix'),
- strip=False,
- required=False,
- )
-
- def clean_subject_prefix(self):
- """
- Strip the leading whitespaces from the subject_prefix form field.
- """
- return self.cleaned_data.get('subject_prefix', '').lstrip()
-
-
- class ListMassSubscription(forms.Form):
- """Form fields to masssubscribe users to a list.
- """
- emails = ListOfStringsField(
- label=_('Emails to mass subscribe'),
- help_text=_(
- 'The following formats are accepted:\n'
- 'jdoe@example.com\n'
- '<jdoe@example.com>\n'
- 'John Doe <jdoe@example.com>\n'
- '"John Doe" <jdoe@example.com>\n'
- 'jdoe@example.com (John Doe)\n'
- 'Use the last three to associate a display name with'
- ' the address\n'),
- )
-
-
- class ListMassRemoval(forms.Form):
-
- """Form fields to remove multiple list users.
- """
- emails = ListOfStringsField(
- label=_('Emails to Unsubscribe'),
- help_text=_('Add one email address on each line'),
- )
-
- class Meta:
-
- """
- Class to define the name of the fieldsets and what should be
- included in each.
- """
- layout = [["Mass Removal", "emails"]]
-
-
- class ListAddBanForm(forms.Form):
- """Ban an email address for a list."""
- # TODO maxking: This form should only accept valid emails or regular
- # expressions. Anything else that doesn't look like a valid email address
- # or regexp for email should not be a valid value for the field. However,
- # checking for that might not be easy.
- email = forms.CharField(
- label=_('Add ban'),
- help_text=_(
- 'You can ban a single email address or use a regular expression '
- 'to match similar email addresses.'),
- error_messages={
- 'required': _('Please enter an email address.'),
- 'invalid': _('Please enter a valid email address.')})
-
-
- class ListHeaderMatchForm(forms.Form):
- """Edit a list's header match."""
-
- HM_ACTION_CHOICES = [(None, _("Default antispam action"))] + \
- [a for a in ACTION_CHOICES if a[0] != 'defer']
-
- header = forms.CharField(
- label=_('Header'),
- help_text=_('Email header to filter on (case-insensitive).'),
- error_messages={
- 'required': _('Please enter a header.'),
- 'invalid': _('Please enter a valid header.')})
- pattern = forms.CharField(
- label=_('Pattern'),
- help_text=_('Regular expression matching the header\'s value.'),
- error_messages={
- 'required': _('Please enter a pattern.'),
- 'invalid': _('Please enter a valid pattern.')})
- action = forms.ChoiceField(
- label=_('Action'),
- error_messages={'invalid': _('Please enter a valid action.')},
- required=False,
- choices=HM_ACTION_CHOICES,
- help_text=_('Action to take when a header matches')
- )
-
-
- class ListHeaderMatchFormset(forms.BaseFormSet):
- def clean(self):
- """Checks that no two header matches have the same order."""
- if any(self.errors):
- # Don't bother validating the formset unless
- # each form is valid on its own
- return
- orders = []
- for form in self.forms:
- try:
- order = form.cleaned_data['ORDER']
- except KeyError:
- continue
- if order in orders:
- raise forms.ValidationError('Header matches must have'
- ' distinct orders.')
- orders.append(order)
-
-
- class MemberModeration(forms.Form):
- """
- Form handling the member's moderation_action.
- """
- moderation_action = forms.ChoiceField(
- widget=forms.Select(),
- label=_('Moderation'),
- required=False,
- choices=[(None, _('List default'))] + list(ACTION_CHOICES),
- help_text=_(
- 'Default action to take when this member posts to the list. \n'
- 'List default -- follow the list\'s default member action. \n'
- 'Hold -- This holds the message for approval by the list '
- 'moderators. \n'
- 'Reject -- this automatically rejects the message by sending a '
- 'bounce notice to the post\'s author. The text of the bounce '
- 'notice can be configured by you. \n'
- 'Discard -- this simply discards the message, with no notice '
- 'sent to the post\'s author. \n'
- 'Accept -- accepts any postings without any further checks. \n'
- 'Defer -- default processing, run additional checks and accept '
- 'the message. \n'))
-
-
- class ChangeSubscriptionForm(forms.Form):
- email = forms.ChoiceField()
-
- def __init__(self, user_emails, *args, **kwargs):
- super(ChangeSubscriptionForm, self).__init__(*args, **kwargs)
- self.fields['email'] = forms.ChoiceField(
- label=_('Select Email'),
- required=False,
- widget=forms.Select(),
- choices=((address, address) for address in user_emails))
- summary.html: |
- {% extends "postorius/base.html" %}
- {% load i18n %}
- {% load bootstrap_tags %}
- {% load nav_helpers %}
-
- {% block head_title %}
- {% trans 'Info' %} | {{ list.fqdn_listname }} - {{ block.super }}
- {% endblock %}
-
- {% block content %}
-
- {% list_nav 'list_summary' 'Summary' %}
-
-
{{ list.settings.description }}
- {% if list.settings.info %}
-
{{ list.settings.info }}
- {% endif %}
-
{% trans 'To contact the list owners, use the following email address:' %} {{ list.settings.owner_address }}
-
- {# Archives #}
- {% if hyperkitty_enabled %}
- {% if not public_archive and not user.is_authenticated %}
-
- {% trans 'You have to login to visit the archives of this list.' %}
-
- {% trans 'You are subscribed to this list with the following address:' %} {{ subscribed_address }}
-
- {% url 'user_list_options' list.list_id as user_list_options_url %}
-
- {% blocktrans %}
- You can manage your subscription on your list options page
- {% endblocktrans %}
-
-
-
-
- {% elif user_request_pending %}
-
{% trans "You have a subscription request pending. If you don't hear back soon, please contact the list owners." %}
- {% else %}
-
{% trans 'Subscribe to this list' %}
-
- {% blocktrans with address=list.settings.join_address %}
- To subscribe you can send an email with 'subscribe' in the subject to
- {{ address }}
- or use the form below:
- {% endblocktrans %}
-
-
- {% endif %}
- {% else %}
-
-
- {% blocktrans %}
- You can also subscribe without creating an account.
- If you wish to do so, please use the form below.
- {% endblocktrans%}
-
-
-
- {% endif %}
-
- {# List metrics #}
- {% if user.is_authenticated %}
- {% if user.is_list_owner or user.is_superuser %}
-
-