mirror of
https://kernel.googlesource.com/pub/scm/linux/kernel/git/tglx/remail.git
synced 2024-11-24 21:32:39 +01:00
remail: Initial import
Signed-off-by: Thomas Gleixner <tglx@linutronix.de>
This commit is contained in:
commit
60f6698e52
48 changed files with 5299 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Normal rules
|
||||||
|
*.bz2
|
||||||
|
*.diff
|
||||||
|
*.gz
|
||||||
|
*.patch
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
*.tar
|
||||||
|
*.xz
|
||||||
|
|
||||||
|
# Editor artifacts
|
||||||
|
*~
|
||||||
|
\#*#
|
||||||
|
|
||||||
|
# Quilt's files
|
||||||
|
patches
|
||||||
|
series
|
||||||
|
.pc/
|
||||||
|
|
||||||
|
# Python artifacts
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Documentation artifacts
|
||||||
|
Documentation/output/
|
14
COPYING
Normal file
14
COPYING
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
remail is provided under:
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
|
||||||
|
Being under the terms of the GNU General Public License version 2 only,
|
||||||
|
according with:
|
||||||
|
|
||||||
|
LICENSES/GPL-2.0
|
||||||
|
|
||||||
|
In addition, other licenses may also apply. Please see:
|
||||||
|
|
||||||
|
Documentation/license-rules.rst
|
||||||
|
|
||||||
|
for more details.
|
20
Documentation/Makefile
Normal file
20
Documentation/Makefile
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SPHINXPROJ = remail
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = output
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
163
Documentation/conf.py
Normal file
163
Documentation/conf.py
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# remail documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Tue May 15 16:12:59 2018.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = []
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['sphinx_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'remail'
|
||||||
|
copyright = u'2018, Thomas Gleixner <tglx@linutronix.de>'
|
||||||
|
author = u'Thomas Gleixner <tglx@linutronix.de>'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = u''
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = u''
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'nature'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['sphinx_static']
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'remaildoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'remail.tex', u'remail Documentation',
|
||||||
|
u'Thomas Gleixner \\textless{}tglx@linutronix.de\\textgreater{}', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
# (master_doc, 'remail', u'remail Documentation',
|
||||||
|
# [author], 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
for f in glob.glob('man1/*.rst'):
|
||||||
|
src = f.replace('.rst', '')
|
||||||
|
dst = src.split('/')[1]
|
||||||
|
man_pages.append((src, dst, u'%s Documentation' % dst, [author], 1))
|
||||||
|
|
||||||
|
for f in glob.glob('man5/*.rst'):
|
||||||
|
src = f.replace('.rst', '')
|
||||||
|
dst = src.split('/')[1]
|
||||||
|
man_pages.append((src, dst, u'%s Documentation' % dst, [author], 5))
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'remail', u'remail Documentation',
|
||||||
|
author, 'remail', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
176
Documentation/configuration.rst
Normal file
176
Documentation/configuration.rst
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_configuration:
|
||||||
|
|
||||||
|
remail configuration guide
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
The configuration files are simple yaml files which only use the very basic
|
||||||
|
yaml components. The yaml file does not have a schema yet, but the
|
||||||
|
configuration parser is doing extensive verification.
|
||||||
|
|
||||||
|
See :ref:`remail_daemon_config_man` for a detailed explanation of the
|
||||||
|
configuration files and the configuration items.
|
||||||
|
|
||||||
|
The examples directory in Documentation contains a full dummy configuration
|
||||||
|
for two lists including fully documented yaml files.
|
||||||
|
|
||||||
|
Work directory
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The work directory structure is fixed and is documented in the man page in
|
||||||
|
the section :ref:`config_dir_struct`.
|
||||||
|
|
||||||
|
A comfortable way to manage the configuration is git. The base
|
||||||
|
configuration directory and the actual list configuration directories are
|
||||||
|
in separate git trees so that they can be maintained by different people
|
||||||
|
with different permissions.
|
||||||
|
|
||||||
|
|
||||||
|
Key considerations
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Private keys
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
In order to encrypt incoming mail and to sign outgoing mail the list daemon
|
||||||
|
needs access to the private GPG and S/MIME keys of each list.
|
||||||
|
|
||||||
|
The keys must be stored without password.
|
||||||
|
|
||||||
|
This is not a security issue because if the keys would have passwords then
|
||||||
|
the password needs to be part of the configuration file.
|
||||||
|
|
||||||
|
If the list configuration or the list server is compromised all bets are
|
||||||
|
off. Having a password on the keys would not make any difference.
|
||||||
|
|
||||||
|
Public keys
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
The public GPG keys of the subscribers in the mailing list specific keyring
|
||||||
|
are always trusted by remail. The list administrator is responsible for
|
||||||
|
verifying the authenticity of the subscribers key, so extensive trust
|
||||||
|
management does not make sense and just complicates the handling.
|
||||||
|
|
||||||
|
remail does not try to update the keyring at any time. This is in the
|
||||||
|
responsibility of the list administrator. Automatic updates are not a
|
||||||
|
really good idea in the light of the recent attacks on the PGP
|
||||||
|
infrastructure.
|
||||||
|
|
||||||
|
The public S/MIME certificates are verified against the CA certificates
|
||||||
|
which are provided and managed by the remail base administrator as part of
|
||||||
|
the base configuration.
|
||||||
|
|
||||||
|
Key storage format
|
||||||
|
------------------
|
||||||
|
|
||||||
|
GPG
|
||||||
|
^^^
|
||||||
|
|
||||||
|
GPG keys are stored using the storage format of gnugp. Create and manage
|
||||||
|
the private keys and the subscriber keyrings with the GPG tool of your
|
||||||
|
choice.
|
||||||
|
|
||||||
|
S/MIME
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
|
S/MIME .key and .crt files are stored in pem format and have to follow
|
||||||
|
the following naming convention.
|
||||||
|
|
||||||
|
Private keys (list keys):
|
||||||
|
email@address.domain.key
|
||||||
|
|
||||||
|
Public certificates (list and subscribers):
|
||||||
|
email@address.domain.crt
|
||||||
|
|
||||||
|
Using git for configuration management
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
As seen above the base configuration directory contains the private keys
|
||||||
|
for the lists. So the administrator has to ensure that the git repository
|
||||||
|
which is used to push the configuration to is properly protected. Also the
|
||||||
|
machine used for updating the base configuration repository should not be
|
||||||
|
accessible by unpriviledged users.
|
||||||
|
|
||||||
|
The incomplete list of things to avoid under all circumstances:
|
||||||
|
|
||||||
|
- Push the base configuration to a public hosted repository server like
|
||||||
|
github, gitlab etc.
|
||||||
|
|
||||||
|
- Keep the base configuration on an easy accessible machine if the data is
|
||||||
|
not sufficiently protected by encryption
|
||||||
|
|
||||||
|
- Keep the base configuration unencrypted on a backup medium.
|
||||||
|
|
||||||
|
The list configuration does not contain strictly secret information, but
|
||||||
|
the pure existance of an incident list and the list of involved people
|
||||||
|
might give a hint in which area the handled issue might be. So in general
|
||||||
|
it is recommended to hide a list configuration repository from public and
|
||||||
|
unpriviledged access as well, but in case of an leak the potential damage
|
||||||
|
is way smaller than the one of leaking the base configuration which
|
||||||
|
contains the private keys.
|
||||||
|
|
||||||
|
|
||||||
|
Base configuration
|
||||||
|
------------------
|
||||||
|
|
||||||
|
To configure the list daemon the following configuration files need to be
|
||||||
|
created or modified:
|
||||||
|
|
||||||
|
- remail.yaml
|
||||||
|
|
||||||
|
remail.yaml:
|
||||||
|
|
||||||
|
The main configuration file for the remail daemon. See
|
||||||
|
:ref:`remail_daemon_config_man` for a detailed information of the file.
|
||||||
|
|
||||||
|
List configuration
|
||||||
|
------------------
|
||||||
|
|
||||||
|
For each enabled mailing list the following configuration files need to be
|
||||||
|
created or modified:
|
||||||
|
|
||||||
|
- list.yaml
|
||||||
|
- template_welcome
|
||||||
|
- template_admin
|
||||||
|
|
||||||
|
list.yaml:
|
||||||
|
|
||||||
|
The list specific configuration file for the mailing list. It contains
|
||||||
|
the subscriber list. See :ref:`remail_daemon_config_man` for a detailed
|
||||||
|
information of the file.
|
||||||
|
|
||||||
|
template_admin:
|
||||||
|
|
||||||
|
The mail template for mail to the list admin(s).
|
||||||
|
|
||||||
|
template_welcome:
|
||||||
|
|
||||||
|
The mail template for mail to welcome new subscribers.
|
||||||
|
|
||||||
|
|
||||||
|
Public mail
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Request or configure a user account for receiving list mail. This mail
|
||||||
|
account acts as a catch all for the list, the list-owner and list-bounce
|
||||||
|
addresses. If more than one mailing list is served by the list daemon then
|
||||||
|
the user mail account can receive all mails for the lists. remail finds the
|
||||||
|
appropriate mailing list for the various addresses.
|
||||||
|
|
||||||
|
remail does not retrieve mail from a public mail server as this is outside
|
||||||
|
the scope of remail and depends on the particular setup. remail expects the
|
||||||
|
incoming mails to be delivered into a maildir. Tools like getmail,
|
||||||
|
fetchmail can handle that as well as SMTP servers.
|
||||||
|
|
||||||
|
|
||||||
|
Local mail transport
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
remail delivers the outgoing mail to the local SMTP server. The
|
||||||
|
configuration for the SMTP server to relay the mails to the public facing
|
||||||
|
machines is outside the scope of this documentation and depends on the SMTP
|
||||||
|
server variant you are using.
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
1
Documentation/examples/conf/.certs/cacert.pem
Normal file
1
Documentation/examples/conf/.certs/cacert.pem
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
1
Documentation/examples/conf/.gnupg/pubring.kbx
Normal file
1
Documentation/examples/conf/.gnupg/pubring.kbx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
41
Documentation/examples/conf/lists/list1/list.yaml
Normal file
41
Documentation/examples/conf/lists/list1/list.yaml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#
|
||||||
|
# Recrypting mailing list configuration file for list1
|
||||||
|
#
|
||||||
|
|
||||||
|
# The list subscribers
|
||||||
|
subscribers:
|
||||||
|
|
||||||
|
# Format:
|
||||||
|
# email address:
|
||||||
|
# name: Real name of the subscriber
|
||||||
|
# enabled: Subscriber is enabled (if omitted defaults to False)
|
||||||
|
# use_smime: True/False (Use S/MIME for encryption. If omitted defaults to False)
|
||||||
|
# fingerprint: GPG fingerprint (Not required when use_smime == True)
|
||||||
|
# gpg_plain: Plain text inline GPG encryption (If omitted defaults to False)
|
||||||
|
# aliases: List of alias addresses which are valid for posting (moderated list)
|
||||||
|
#
|
||||||
|
# Note: Use spaces for spacing not tabs
|
||||||
|
|
||||||
|
dev1@some.domain:
|
||||||
|
name: Developer One
|
||||||
|
enabled: True
|
||||||
|
use_smime: True
|
||||||
|
|
||||||
|
dev2@some.domain:
|
||||||
|
name: Developer Two
|
||||||
|
enabled: False
|
||||||
|
|
||||||
|
dev3@other.domain:
|
||||||
|
name: Developer Three
|
||||||
|
enabled: True
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
aliases:
|
||||||
|
- dev3@dev3.domain
|
||||||
|
- hckr@dev3.domain
|
||||||
|
|
||||||
|
dev4@other.domain:
|
||||||
|
name: Developer Four
|
||||||
|
enabled: True
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
gpg_plain: True
|
||||||
|
|
6
Documentation/examples/conf/lists/list1/template_admin
Normal file
6
Documentation/examples/conf/lists/list1/template_admin
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: [LIST1] Remail failure report
|
||||||
|
User-Agent: Remail
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Disposition: inline
|
||||||
|
|
||||||
|
The following problems have been encountered with the list list1:
|
26
Documentation/examples/conf/lists/list1/template_welcome
Normal file
26
Documentation/examples/conf/lists/list1/template_welcome
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Subject: Welcome to the list1 mailing-list
|
||||||
|
User-Agent: Remail
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Disposition: inline
|
||||||
|
|
||||||
|
Dear $NAME!
|
||||||
|
|
||||||
|
This is an automated email from your friendly mailing-list update bot. This
|
||||||
|
email is sent to you because you got (re-)subscribed to the list1
|
||||||
|
mailing-list.
|
||||||
|
|
||||||
|
This email to you is signed either with the list's PGP key or the list's
|
||||||
|
S/MIME certificate depending on your encryption preference, so your
|
||||||
|
email-client should have already retrieved the PGP key or the S/MIME
|
||||||
|
certificate for you.
|
||||||
|
|
||||||
|
The list's email address is: list1@list.domain
|
||||||
|
|
||||||
|
To send email to the list, encrypt it either with the list's PGP key or the
|
||||||
|
list's S/MIME certificate. Note, that the subject line of the email is not
|
||||||
|
encrypted, so you should be careful about that.
|
||||||
|
|
||||||
|
If you have questions, please contact the list administrator by email. The
|
||||||
|
administrators email address is: list1-owner@list.domain
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
41
Documentation/examples/conf/lists/list2/list.yaml
Normal file
41
Documentation/examples/conf/lists/list2/list.yaml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#
|
||||||
|
# Recrypting mailing list configuration file for list2
|
||||||
|
#
|
||||||
|
|
||||||
|
# The list subscribers
|
||||||
|
subscribers:
|
||||||
|
|
||||||
|
# Format:
|
||||||
|
# email address:
|
||||||
|
# name: Real name of the subscriber
|
||||||
|
# enabled: Subscriber is enabled (if omitted defaults to False)
|
||||||
|
# use_smime: True/False (Use S/MIME for encryption. If omitted defaults to False)
|
||||||
|
# fingerprint: GPG fingerprint (Not required when use_smime == True)
|
||||||
|
# gpg_plain: Plain text inline GPG encryption (If omitted defaults to False)
|
||||||
|
# aliases: List of alias addresses which are valid for posting (moderated list)
|
||||||
|
#
|
||||||
|
# Note: Use spaces for spacing not tabs
|
||||||
|
|
||||||
|
dev1@some.domain:
|
||||||
|
name: Developer One
|
||||||
|
enabled: True
|
||||||
|
use_smime: True
|
||||||
|
|
||||||
|
dev2@some.domain:
|
||||||
|
name: Developer Two
|
||||||
|
enabled: False
|
||||||
|
|
||||||
|
dev3@other.domain:
|
||||||
|
name: Developer Three
|
||||||
|
enabled: True
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
aliases:
|
||||||
|
- dev3@dev3.domain
|
||||||
|
- hckr@dev3.domain
|
||||||
|
|
||||||
|
dev4@other.domain:
|
||||||
|
name: Developer Four
|
||||||
|
enabled: True
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
gpg_plain: True
|
||||||
|
|
6
Documentation/examples/conf/lists/list2/template_admin
Normal file
6
Documentation/examples/conf/lists/list2/template_admin
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: [LIST2] Remail failure report
|
||||||
|
User-Agent: Remail
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Disposition: inline
|
||||||
|
|
||||||
|
The following problems have been encountered with the list list2:
|
26
Documentation/examples/conf/lists/list2/template_welcome
Normal file
26
Documentation/examples/conf/lists/list2/template_welcome
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Subject: Welcome to the list2 mailing-list
|
||||||
|
User-Agent: Remail
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Disposition: inline
|
||||||
|
|
||||||
|
Dear $NAME!
|
||||||
|
|
||||||
|
This is an automated email from your friendly mailing-list update bot. This
|
||||||
|
email is sent to you because you got (re-)subscribed to the list2
|
||||||
|
mailing-list.
|
||||||
|
|
||||||
|
This email to you is signed either with the list's PGP key or the list's
|
||||||
|
S/MIME certificate depending on your encryption preference, so your
|
||||||
|
email-client should have already retrieved the PGP key or the S/MIME
|
||||||
|
certificate for you.
|
||||||
|
|
||||||
|
The list's email address is: list2@list.domain
|
||||||
|
|
||||||
|
To send email to the list, encrypt it either with the list's PGP key or the
|
||||||
|
list's S/MIME certificate. Note, that the subject line of the email is not
|
||||||
|
encrypted, so you should be careful about that.
|
||||||
|
|
||||||
|
If you have questions, please contact the list administrator by email. The
|
||||||
|
administrators email address is: list2-owner@list.domain
|
||||||
|
|
||||||
|
|
1
Documentation/examples/conf/lock
Normal file
1
Documentation/examples/conf/lock
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
116
Documentation/examples/conf/remail.yaml
Normal file
116
Documentation/examples/conf/remail.yaml
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
#
|
||||||
|
# Remail base configuration file for the remail list server
|
||||||
|
#
|
||||||
|
# Note: YAML requires spaces. Do not use TABs
|
||||||
|
#
|
||||||
|
|
||||||
|
# Enabled. If False all other items are ignored. Set to true to get it starting
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
# Set to True to enable SMTP delivery. Set only to False for testing or
|
||||||
|
# troubleshooting
|
||||||
|
use_smtp: True
|
||||||
|
|
||||||
|
# S/MIME
|
||||||
|
smime:
|
||||||
|
# Verify CA certs. Only disable for troubleshooting
|
||||||
|
verify: True
|
||||||
|
|
||||||
|
# GPG
|
||||||
|
gpg:
|
||||||
|
# Trust your keys always. That avoids manual fiddling with trust-db
|
||||||
|
always_trust: True
|
||||||
|
# List mail should always be signed with the lists key
|
||||||
|
sign: True
|
||||||
|
|
||||||
|
# The mailing lists
|
||||||
|
lists:
|
||||||
|
|
||||||
|
# The list name
|
||||||
|
list1:
|
||||||
|
|
||||||
|
# Set to True if the list is enabled
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
# Set to True if the list should be moderated
|
||||||
|
# If set, only subscribers can post to the list
|
||||||
|
# Non subscriber mails are forwarded to the list admins
|
||||||
|
moderated: True
|
||||||
|
|
||||||
|
archive:
|
||||||
|
use_maildir: True
|
||||||
|
# Set to true if the incoming mails should be archived
|
||||||
|
# locally in a mailbox/dir unmodified before decryption
|
||||||
|
archive_incoming: True
|
||||||
|
# Set to true if the incoming mails should be archived
|
||||||
|
# locally in a mailbox/dir after decrypting them
|
||||||
|
archive_plain: True
|
||||||
|
|
||||||
|
# The list account itself
|
||||||
|
listaccount:
|
||||||
|
list1@list.domain:
|
||||||
|
name: list1
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
|
||||||
|
# The list admins
|
||||||
|
admins:
|
||||||
|
# Format:
|
||||||
|
# email address:
|
||||||
|
# name: Real name of the subscriber
|
||||||
|
# enabled: Subscriber is enabled (if omitted defaults to False)
|
||||||
|
# use_smime: True/False (Use S/MIME for encryption. If omitted defaults to False)
|
||||||
|
# fingerprint: GPG fingerprint (Not required when use_smime == True)
|
||||||
|
# gpg_plain: Plain text inline GPG encryption (If omitted defaults to False)
|
||||||
|
admin1@admin.domain:
|
||||||
|
name: Admin one
|
||||||
|
use_smime: True
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
admin2@admin2.domain:
|
||||||
|
name: Admin2
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
gpg_plain: True
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
# The list name
|
||||||
|
list2:
|
||||||
|
# Set to True if the list is enabled
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
# Set to True if the list should be moderated
|
||||||
|
# If set, only subscribers can post to the list
|
||||||
|
# Non subscriber mails are forwarded to the list admins
|
||||||
|
moderated: True
|
||||||
|
|
||||||
|
archive:
|
||||||
|
use_maildir: True
|
||||||
|
# Set to true if the incoming mails should be archived
|
||||||
|
# locally in a mailbox/dir unmodified before decryption
|
||||||
|
archive_incoming: True
|
||||||
|
# Set to true if the incoming mails should be archived
|
||||||
|
# locally in a mailbox/dir after decrypting them
|
||||||
|
archive_plain: False
|
||||||
|
|
||||||
|
# The list account itself
|
||||||
|
listaccount:
|
||||||
|
list1@list.domain:
|
||||||
|
name: list1
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
|
||||||
|
# The list admins
|
||||||
|
admins:
|
||||||
|
# Format:
|
||||||
|
# email address:
|
||||||
|
# name: Real name of the subscriber
|
||||||
|
# enabled: Subscriber is enabled (if omitted defaults to False)
|
||||||
|
# use_smime: True/False (Use S/MIME for encryption. If omitted defaults to False)
|
||||||
|
# fingerprint: GPG fingerprint (Not required when use_smime == True)
|
||||||
|
# gpg_plain: Plain text inline GPG encryption (If omitted defaults to False)
|
||||||
|
admin2@admin2.domain:
|
||||||
|
name: Admin2
|
||||||
|
fingerprint: 40_CHARACTER_PGP_FINGERPRINT
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
admin3@admin3.domain:
|
||||||
|
name: Admin three
|
||||||
|
enabled: False
|
80
Documentation/index.rst
Normal file
80
Documentation/index.rst
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. remail documentation master file
|
||||||
|
|
||||||
|
Welcome to remail's documentation!
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
introduction.rst
|
||||||
|
|
||||||
|
|
||||||
|
Licensing
|
||||||
|
---------
|
||||||
|
|
||||||
|
Describes the licensing rules for the remail source code.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
license-rules.rst
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Installation guidelines
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
installation.rst
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Configuration guidelines
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
configuration.rst
|
||||||
|
|
||||||
|
Operation
|
||||||
|
----------
|
||||||
|
|
||||||
|
Information about operation
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
operation.rst
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Usage guidelines (man pages)
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
man1/remail_daemon.rst
|
||||||
|
man1/remail_chkcfg.rst
|
||||||
|
man5/remail_daemon.config.rst
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
58
Documentation/installation.rst
Normal file
58
Documentation/installation.rst
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_installation:
|
||||||
|
|
||||||
|
remail installation guide
|
||||||
|
=========================
|
||||||
|
|
||||||
|
General
|
||||||
|
-------
|
||||||
|
|
||||||
|
The recommended installation procedure is via the package management system
|
||||||
|
of your distribution. Manual installation is surely possible and the
|
||||||
|
various bits and pieces including their default or recommended locations on
|
||||||
|
the target are documented in the packaging files.
|
||||||
|
|
||||||
|
By default the daemon is disabled as it requires configuration.
|
||||||
|
|
||||||
|
|
||||||
|
Protecting the system
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Run the list daemon on a machine which is not publicly accessible and
|
||||||
|
ensure that no untrusted users can access it. It can be run in a VM with
|
||||||
|
the sole purpose of running the encrypted mailing list. As the private keys
|
||||||
|
of the list are stored on that machine the setup requires deep
|
||||||
|
understanding of IT security. It's outside the scope of this documentation
|
||||||
|
to provide guidance on this.
|
||||||
|
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
remail requires Python 3 and depends on the following python packages:
|
||||||
|
|
||||||
|
- python3-M2Crypto
|
||||||
|
- python3-gnupg
|
||||||
|
- python3-yaml
|
||||||
|
|
||||||
|
Note, that on Debian 10 python3-M2Crypto is not available, but can be
|
||||||
|
installed as a Debian package from the testing repository.
|
||||||
|
|
||||||
|
Further the systems needs to have:
|
||||||
|
|
||||||
|
- A mail transport agent which handles the transport of the mails to an
|
||||||
|
outgoing SMTP server.
|
||||||
|
|
||||||
|
- A mail retriever agent or some other mechanism which can fetch or
|
||||||
|
handle incoming mail and deliver it to remails maildir.
|
||||||
|
|
||||||
|
The initial deployment of the list daemon which was used to handle
|
||||||
|
development of mitigations for embargoed hardware security vulnerabilities
|
||||||
|
uses postfix and getmail, but that's only one of various possibilites.
|
||||||
|
|
||||||
|
As the choice of tools depends on the setup and the situation under which
|
||||||
|
the system is deployed, there is no documentation provided here. It's a
|
||||||
|
prerequisite that an administrator who wants to deploy remail is familiar
|
||||||
|
with these kind of tools.
|
||||||
|
|
95
Documentation/introduction.rst
Normal file
95
Documentation/introduction.rst
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_introduction:
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
remail is a very simplistic mailing list tool which provides encryption.
|
||||||
|
|
||||||
|
Why yet another mailing list tool?
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
The handling of the embargoed hardware security issues made it necessary
|
||||||
|
to have a encrypted mailing list. Managing Cc lists manually and dealing
|
||||||
|
with the odd GPG integrations in the mail clients is just not a workable
|
||||||
|
solution.
|
||||||
|
|
||||||
|
There are only a few Open Source implementations of encrypted mailing lists
|
||||||
|
available. Some of them are abandoned projects. The alive ones have their
|
||||||
|
own set of shortcomings. One of them supports only S/MIME. The other
|
||||||
|
supports only PGP, but did not survive testing in a real world
|
||||||
|
deployment. Repairing ruby code and dis-tangling the thing from its weird
|
||||||
|
daemons and other over-engineered features was certainly not a project for
|
||||||
|
the weekend.
|
||||||
|
|
||||||
|
After thinking about it for a while, I recognized that for the purpose at
|
||||||
|
hand the tool can be very simplistic. Using existing tools like getmail and
|
||||||
|
SMTP servers plus the knowledge about the gory details of emails which I
|
||||||
|
gained by writing mail handling scripts in python for my daily work, made
|
||||||
|
it possible to implement it in a rather short time.
|
||||||
|
|
||||||
|
|
||||||
|
What it does
|
||||||
|
------------
|
||||||
|
|
||||||
|
remail reads mail from a maildir, decrypts it with the mailing list private
|
||||||
|
key and re-encrypts it for every enabled subscriber. The resulting mails are
|
||||||
|
delivered to the localhost's SMTP server.
|
||||||
|
|
||||||
|
remail supports S/MIME and PGP on both ends.
|
||||||
|
|
||||||
|
No MUA functionality and no complicated SMTP delivery mechanisms are
|
||||||
|
required. They all exist in well maintained tools already.
|
||||||
|
|
||||||
|
The list configuration is a simple yaml file and is maintained manually
|
||||||
|
with your favorite text editor. Building tools around that is not part of
|
||||||
|
this project and there are many existing ways to handle that conveniently.
|
||||||
|
|
||||||
|
The proven in use mechanism is to have the configuration files in git and
|
||||||
|
let the mailing list system either check for updates regularly or get some
|
||||||
|
external notification that a new configuration is available.
|
||||||
|
|
||||||
|
|
||||||
|
What it does not
|
||||||
|
----------------
|
||||||
|
|
||||||
|
It does not care about mail transport on the receiving side, no POP or IMAP
|
||||||
|
support as this can be handled by tools like getmail.
|
||||||
|
|
||||||
|
It does not care about complex mail transport on the sending side as this
|
||||||
|
can be handled by SMTP servers like postfix or exim.
|
||||||
|
|
||||||
|
It has no integration into SMTP server transports or filters because it
|
||||||
|
makes no sense to deploy such a sensitive mechanism on a public facing
|
||||||
|
machine.
|
||||||
|
|
||||||
|
It does not come with GUI and managenent tools. A crypto mailing list is
|
||||||
|
subscribers only for obvious reasons and not meant to handle a large amount
|
||||||
|
of subscribers. It's meant for secure communications of a small group of
|
||||||
|
people without having the hassle of keeping Cc lists up to date and
|
||||||
|
encrypting for every recipient.
|
||||||
|
|
||||||
|
|
||||||
|
How it works
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. image:: remail.svg
|
||||||
|
|
||||||
|
The mailing list has the usual post, bounce and owner email addresses on a
|
||||||
|
public mail server. These addresses are aliased to a user account. The
|
||||||
|
mails are stored in the users inbox or forwarded to a protected machine.
|
||||||
|
|
||||||
|
The mailing list software runs on a protected and separated machine behind
|
||||||
|
firewalls and access barriers like any other sensitive application.
|
||||||
|
|
||||||
|
The mail is retrieved from the public facing machine by any of the existing
|
||||||
|
mechanisms, e.g. getmail, fetchmail or any MTA which can deliver mail to a
|
||||||
|
maildir. remail does not implement any transport on the incoming side as
|
||||||
|
there are good tools available which can handle the requirements of a
|
||||||
|
particular setup.
|
||||||
|
|
||||||
|
The list daemon retrieves the mails from the maildir and selects the
|
||||||
|
appropriate list via the 'To' header. It then decrypts the incoming mail,
|
||||||
|
re-encrypts it for each subscriber and delivers it to the local SMTP server
|
||||||
|
which takes care of relaying it to the public facing SMTP server.
|
161
Documentation/license-rules.rst
Normal file
161
Documentation/license-rules.rst
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_licensing_rules:
|
||||||
|
|
||||||
|
remail licensing rules
|
||||||
|
======================
|
||||||
|
|
||||||
|
The remail project is provided under the terms of the GNU General Public
|
||||||
|
License version 2 only (GPL-2.0-only), as provided in LICENSES/GPL-2.0.
|
||||||
|
|
||||||
|
This documentation file provides a description of how each source file
|
||||||
|
should be annotated to make its license clear and unambiguous.
|
||||||
|
It doesn't replace the projects license.
|
||||||
|
|
||||||
|
The license described in the COPYING file applies to the project source
|
||||||
|
as a whole, though individual source files can have a different license
|
||||||
|
which is required to be compatible with the GPL-2.0-only::
|
||||||
|
|
||||||
|
GPL-2.0-or-later : GNU General Public License v2.0 or later
|
||||||
|
|
||||||
|
Aside from that, individual files can be provided under a dual license,
|
||||||
|
e.g. one of the compatible GPL variants and alternatively under a
|
||||||
|
permissive license like BSD, MIT etc.
|
||||||
|
|
||||||
|
The common way of expressing the license of a source file is to add the
|
||||||
|
matching boilerplate text into the top comment of the file. Due to
|
||||||
|
formatting, typos etc. these "boilerplates" are hard to validate for
|
||||||
|
tools which are used in the context of license compliance.
|
||||||
|
|
||||||
|
An alternative to boilerplate text is the use of Software Package Data
|
||||||
|
Exchange (SPDX) license identifiers in each source file. SPDX license
|
||||||
|
identifiers are machine parsable and precise shorthands for the license
|
||||||
|
under which the content of the file is contributed. SPDX license
|
||||||
|
identifiers are managed by the SPDX Workgroup at the Linux Foundation and
|
||||||
|
have been agreed on by partners throughout the industry, tool vendors, and
|
||||||
|
legal teams. For further information see https://spdx.org/
|
||||||
|
|
||||||
|
The remail prokect requires the precise SPDX identifier in all source
|
||||||
|
files. The valid identifiers used in the remail project are explained in
|
||||||
|
the section `License identifiers`_ and have been retrieved from the
|
||||||
|
official SPDX license list at https://spdx.org/licenses/ along with the
|
||||||
|
license texts.
|
||||||
|
|
||||||
|
License identifier syntax
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
1. Placement:
|
||||||
|
|
||||||
|
The SPDX license identifier in source files shall be added at the first
|
||||||
|
possible line in a file which can contain a comment. For the majority
|
||||||
|
or files this is the first line, except for executable scripts which
|
||||||
|
require the '#!PATH_TO_INTERPRETER' in the first line. For those
|
||||||
|
scripts the SPDX identifier goes into the second line.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
2. Style:
|
||||||
|
|
||||||
|
The SPDX license identifier is added in form of a comment. The comment
|
||||||
|
style depends on the file type::
|
||||||
|
|
||||||
|
scripts: # SPDX-License-Identifier: <SPDX License Expression>
|
||||||
|
.py: # SPDX-License-Identifier: <SPDX License Expression>
|
||||||
|
.rst: .. SPDX-License-Identifier: <SPDX License Expression>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
3. Syntax:
|
||||||
|
|
||||||
|
A <SPDX License Expression> is either an SPDX short form license
|
||||||
|
identifier found on the SPDX License List. When multiple licenses apply,
|
||||||
|
an expression consists of keywords "AND", "OR" separating
|
||||||
|
sub-expressions and surrounded by "(", ")" .
|
||||||
|
|
||||||
|
License identifiers for licenses like [L]GPL with the 'or later' option
|
||||||
|
are constructed by using a "-or-later" for indicating the 'or later'
|
||||||
|
option.::
|
||||||
|
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
OR should be used if the file is dual licensed and only one license is
|
||||||
|
to be selected. For example::
|
||||||
|
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only OR BSD-3-Clause
|
||||||
|
|
||||||
|
AND should be used if the file has multiple licenses whose terms all
|
||||||
|
apply to use the file. For example, if code is inherited from another
|
||||||
|
project and permission has been given to put it in the remail project,
|
||||||
|
but the original license terms need to remain in effect::
|
||||||
|
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only AND MIT
|
||||||
|
|
||||||
|
|
||||||
|
License identifiers
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The licenses currently used, as well as the licenses for code added to the
|
||||||
|
project can be found in the LICENSES directory.
|
||||||
|
|
||||||
|
Whenever possible these licenses should be used as they are known to be
|
||||||
|
fully compatible and widely used.
|
||||||
|
|
||||||
|
The files in this directory contain the full license text and metatags.
|
||||||
|
The file names are identical to the SPDX license identifier which shall be
|
||||||
|
used for the license in source files.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
LICENSES/GPL-2.0
|
||||||
|
|
||||||
|
Contains the GPL version 2 license text and the required metatags:
|
||||||
|
|
||||||
|
Metatags:
|
||||||
|
|
||||||
|
The following meta tags must be available in a license file:
|
||||||
|
|
||||||
|
- Valid-License-Identifier:
|
||||||
|
|
||||||
|
One or more lines which declare which License Identifiers are valid
|
||||||
|
inside the project to reference this particular license text. Usually
|
||||||
|
this is a single valid identifier, but e.g. for licenses with the 'or
|
||||||
|
later' options two identifiers are valid.
|
||||||
|
|
||||||
|
- SPDX-URL:
|
||||||
|
|
||||||
|
The URL of the SPDX page which contains additional information related
|
||||||
|
to the license.
|
||||||
|
|
||||||
|
- Usage-Guidance:
|
||||||
|
|
||||||
|
Freeform text for usage advice. The text must include correct examples
|
||||||
|
for the SPDX license identifiers as they should be put into source
|
||||||
|
files according to the `License identifier syntax`_ guidelines.
|
||||||
|
|
||||||
|
- License-Text:
|
||||||
|
|
||||||
|
All text after this tag is treated as the original license text
|
||||||
|
|
||||||
|
File format examples::
|
||||||
|
|
||||||
|
Valid-License-Identifier: GPL-2.0-only
|
||||||
|
Valid-License-Identifier: GPL-2.0-or-later
|
||||||
|
SPDX-URL: https://spdx.org/licenses/GPL-2.0-only.html
|
||||||
|
Usage-Guide:
|
||||||
|
To use this license in source code, put one of the following SPDX
|
||||||
|
tag/value pairs into a comment according to the placement
|
||||||
|
guidelines in the licensing rules documentation.
|
||||||
|
For 'GNU General Public License (GPL) version 2 only' use:
|
||||||
|
SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
For 'GNU General Public License (GPL) version 2 or any later version' use:
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
License-Text:
|
||||||
|
Full license text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
All SPDX license identifiers must have a corresponding file in the LICENSE
|
||||||
|
subdirectory. This is required to allow tool verification and to have the
|
||||||
|
licenses ready to read and extract right from the source, which is
|
||||||
|
recommended by various FOSS organizations, e.g. the `FSFE REUSE initiative
|
||||||
|
<https://reuse.software/>`_.
|
69
Documentation/man1/remail_chkcfg.rst
Normal file
69
Documentation/man1/remail_chkcfg.rst
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_chkcfg_man:
|
||||||
|
|
||||||
|
|
||||||
|
remail_chkcfg manual page
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
|
||||||
|
**remail_chkcfg** [*options*] config_file
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:program:`remail_chkcfg`, A program to check and display show the
|
||||||
|
configuration of a crypto mailing list
|
||||||
|
|
||||||
|
It reads the configuration file, verifies for correctness and displays the
|
||||||
|
resulting aggregated configuration in simple form.
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
Show this help message and exit
|
||||||
|
|
||||||
|
-e, --enabled
|
||||||
|
Show a pretty printed tabular list of names and email addresses of all
|
||||||
|
enabled subscribers in a list specific configuration file. Implies -lnq.
|
||||||
|
|
||||||
|
-l, --list
|
||||||
|
Configuration file is a list specific configuration which contains only
|
||||||
|
the subscribers
|
||||||
|
|
||||||
|
-n, --nokeys
|
||||||
|
Do not check for GPG and S/MIME keys and certs
|
||||||
|
|
||||||
|
-q, --quiet
|
||||||
|
Quiet mode. Do not show the configuration. Only check for correctness
|
||||||
|
|
||||||
|
-v, --verbose
|
||||||
|
Enable verbose logging.
|
||||||
|
|
||||||
|
-V, --version
|
||||||
|
Display version information
|
||||||
|
|
||||||
|
|
||||||
|
Invocation
|
||||||
|
----------
|
||||||
|
|
||||||
|
The program expects a properly populated configuration directory at the
|
||||||
|
place where the config_file is. If the program is invoked from outside the
|
||||||
|
configuration directory the program changes into the configuration
|
||||||
|
directory according to the directory part of the config_file argument.
|
||||||
|
|
||||||
|
If the configuration file is the base configuration file the program
|
||||||
|
expects the list configuration files of the enabled lists to be available
|
||||||
|
in the lists subdirectory.
|
||||||
|
|
||||||
|
If it is invoked with a list specific configuration file it only checks and
|
||||||
|
shows this part. If not disabled it checks for the availability and validity
|
||||||
|
of the keys for each subscriber.
|
||||||
|
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
:manpage:`remail_daemon.config(5)`
|
58
Documentation/man1/remail_daemon.rst
Normal file
58
Documentation/man1/remail_daemon.rst
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_daemon_man:
|
||||||
|
|
||||||
|
remail_daemon manual page
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
|
||||||
|
**remail_daemon** [*options*] config_file
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:program:`remail_daemon`, The daemon for running an encrypted mailing
|
||||||
|
list. It reads the configuration file from the command line.
|
||||||
|
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
Show this help message and exit
|
||||||
|
|
||||||
|
-s syslog, --syslog
|
||||||
|
Use syslog for logging. Default is stderr
|
||||||
|
|
||||||
|
-v, --verbose
|
||||||
|
Enable verbose logging.
|
||||||
|
|
||||||
|
-V, --version
|
||||||
|
Display version information
|
||||||
|
|
||||||
|
|
||||||
|
Configuration file
|
||||||
|
------------------
|
||||||
|
|
||||||
|
remail_daemon reads the configuration file which was handed in on the
|
||||||
|
command line. The configuration file is a simple yaml file. Non-mandatory
|
||||||
|
configuration options which are not in the configuration file are set to
|
||||||
|
the default values.
|
||||||
|
|
||||||
|
See the configuration file man page for detailed information.
|
||||||
|
|
||||||
|
|
||||||
|
Work directory
|
||||||
|
--------------
|
||||||
|
|
||||||
|
remail daemon assumes that the configuration file is in the work directory
|
||||||
|
which has a defined layout and content. The directory structure is
|
||||||
|
documented in the full remail documentation along with hints how to manage
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
:manpage:`remail_daemon.config(5)`
|
430
Documentation/man5/remail_daemon.config.rst
Normal file
430
Documentation/man5/remail_daemon.config.rst
Normal file
|
@ -0,0 +1,430 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_daemon_config_man:
|
||||||
|
|
||||||
|
remail_daemon.config manual page
|
||||||
|
================================
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
|
||||||
|
remail.yaml
|
||||||
|
list.yaml
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
|
||||||
|
This manual describes the configuration file format for the remail daemon.
|
||||||
|
|
||||||
|
The configuration files are using yaml syntax. There are two types of
|
||||||
|
configuration files:
|
||||||
|
|
||||||
|
1) Base configuration file
|
||||||
|
|
||||||
|
The default configuration file is located in the base configuration
|
||||||
|
directory which is also the base work directory.
|
||||||
|
|
||||||
|
2) List configuration file
|
||||||
|
|
||||||
|
Per mailing list configuration file.
|
||||||
|
|
||||||
|
Non-mandatory options are set to the builtin defaults if they are not
|
||||||
|
available in the configuration files.
|
||||||
|
|
||||||
|
.. _config_dir_struct:
|
||||||
|
|
||||||
|
Configuration directory structure
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
The configuration directory structure is fixed and looks like this::
|
||||||
|
|
||||||
|
├── .certs
|
||||||
|
│ ├── cacert.pem
|
||||||
|
│ ├── list1@your.domain.key
|
||||||
|
│ ├── list2@your.domain.key
|
||||||
|
│ ├── admin1@some.domain.crt
|
||||||
|
│ ├── admin2@other.domain.crt
|
||||||
|
├── .git
|
||||||
|
├── .gnupg
|
||||||
|
│ ├── private-keys-v1.d
|
||||||
|
│ │ ├── XYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYX1.key
|
||||||
|
│ │ ├── YXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXYXY2.key
|
||||||
|
│ ├── pubring.kbx
|
||||||
|
├── lists
|
||||||
|
│ ├── list1
|
||||||
|
│ │ ├── .certs
|
||||||
|
│ │ │ ├── list1@your.domain.crt
|
||||||
|
│ │ │ ├── subscr1-1@subscr1.domain.crt
|
||||||
|
│ │ │ ├── subscr1-2@subscr2.domain.crt
|
||||||
|
│ │ │ ├── admin1@some.domain.crt
|
||||||
|
│ │ ├── .git
|
||||||
|
│ │ ├── .gnupg
|
||||||
|
│ │ │ └── pubring.kbx
|
||||||
|
│ │ ├── list.yaml
|
||||||
|
│ │ ├── template_admin
|
||||||
|
│ │ └── template_welcome
|
||||||
|
│ ├── list2
|
||||||
|
│ │ ├── .certs
|
||||||
|
│ │ │ ├── list2@your.domain.crt
|
||||||
|
│ │ │ ├── subscr2-1@subscr1.domain.crt
|
||||||
|
│ │ │ ├── subscr2-2@subscr2.domain.crt
|
||||||
|
│ │ │ ├── admin2@other.domain.crt
|
||||||
|
│ │ ├── .git
|
||||||
|
│ │ ├── .gnupg
|
||||||
|
│ │ │ └── pubring.kbx
|
||||||
|
│ │ ├── list.yaml
|
||||||
|
│ │ ├── template_admin
|
||||||
|
│ │ └── template_welcome
|
||||||
|
├── lock
|
||||||
|
├── maildir
|
||||||
|
│ ├── cur
|
||||||
|
│ ├── new
|
||||||
|
│ └── tmp
|
||||||
|
├── maildir.frozen
|
||||||
|
│ ├── cur
|
||||||
|
│ ├── new
|
||||||
|
│ └── tmp
|
||||||
|
└── remail.yaml
|
||||||
|
|
||||||
|
The base directory contains:
|
||||||
|
|
||||||
|
- The main configuration file (remail.yaml).
|
||||||
|
|
||||||
|
- The S/MIME directory (.certs).
|
||||||
|
|
||||||
|
.. warning:: It contains the private S/MIME keys of the lists.
|
||||||
|
|
||||||
|
- The GPG directory (.gnupg).
|
||||||
|
|
||||||
|
.. warning:: It contains the private GPG keys of the lists.
|
||||||
|
|
||||||
|
- The maildir directory to which the incoming mail gets delivered by a
|
||||||
|
MTA or MRA.
|
||||||
|
|
||||||
|
- The maildir.frozen directory to which unprocessed or failed mail
|
||||||
|
gets moved to.
|
||||||
|
|
||||||
|
- A lock file which can be used to protect a reload against a concurrent
|
||||||
|
configuration update.
|
||||||
|
|
||||||
|
- The lists directory under which the list specific configuration directories
|
||||||
|
are located.
|
||||||
|
|
||||||
|
The list directories contain:
|
||||||
|
|
||||||
|
- The list configuration file (list.yaml)
|
||||||
|
|
||||||
|
- The S/MIME directory (.certs). It contains the public S/MIME keys of the
|
||||||
|
list subscribers and of the list itself.
|
||||||
|
|
||||||
|
- The GPG directory (.gnupg). It contains the public GPG keys of the list
|
||||||
|
subscribers and of the list itself.
|
||||||
|
|
||||||
|
|
||||||
|
Base configuration items
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The structure of the base configuration file is::
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
enabled: False
|
||||||
|
use_smtp: False
|
||||||
|
smime:
|
||||||
|
...
|
||||||
|
gpg:
|
||||||
|
...
|
||||||
|
lists:
|
||||||
|
list1:
|
||||||
|
enabled: True
|
||||||
|
moderated: True
|
||||||
|
archive:
|
||||||
|
...
|
||||||
|
listaccount:
|
||||||
|
...
|
||||||
|
admins:
|
||||||
|
ad1@min.domain:
|
||||||
|
...
|
||||||
|
adN@min.domain:
|
||||||
|
...
|
||||||
|
listN:
|
||||||
|
...
|
||||||
|
|
||||||
|
Base items:
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
enabled: False
|
||||||
|
use_smtp: False
|
||||||
|
|
||||||
|
enabled:
|
||||||
|
|
||||||
|
Optional item to enable or disable the daemon. If the item is set to
|
||||||
|
False then no other options are evaluated and the daemon sleeps waiting
|
||||||
|
for termination or reconfiguration. If True, the rest or the options is
|
||||||
|
evaluated.
|
||||||
|
|
||||||
|
Optional item which defaults to False
|
||||||
|
|
||||||
|
use_smtp:
|
||||||
|
|
||||||
|
Set to True to enable mail delivery via SMTP to the SMTP server on
|
||||||
|
localhost. The SMTP server is responsible for relaying the mails to a
|
||||||
|
public mail server. remail does not implement any other transport for
|
||||||
|
outgoing mail and the target server is therefore not configurable.
|
||||||
|
|
||||||
|
If False the encrypted mails are delivered to stdout. That's mainly a
|
||||||
|
development option which is not meant for production use.
|
||||||
|
|
||||||
|
Optional time which defaults to False
|
||||||
|
|
||||||
|
|
||||||
|
S/MIME options:
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
smime:
|
||||||
|
verify: True
|
||||||
|
sign: True
|
||||||
|
|
||||||
|
verify:
|
||||||
|
|
||||||
|
When handling S/MIME encrypted mail then the validity of the senders key
|
||||||
|
is by default verified against the CA certs. If set to False this
|
||||||
|
verification is disabled. Disable this only in extreme situations and
|
||||||
|
consider the consequences.
|
||||||
|
|
||||||
|
sign:
|
||||||
|
|
||||||
|
Sign the mails sent to S/MIME recipients with the lists key. Enabled by
|
||||||
|
default as this is the recommended way to send S/MIME mail. If disabled
|
||||||
|
then the public certificate of the list is not part of the welcome
|
||||||
|
message which is sent to new recipients.
|
||||||
|
|
||||||
|
GPG options:
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
gpg:
|
||||||
|
always_trust: True
|
||||||
|
sign: True
|
||||||
|
|
||||||
|
always_trust:
|
||||||
|
|
||||||
|
The public keyring of a list is managed by the list administrator. To
|
||||||
|
avoid having to manually tweak the trust DB, it's possible to force
|
||||||
|
trust mode on the keyring with this option. Defaults to True as the
|
||||||
|
trust establishment is the responsibility of the list administrator
|
||||||
|
anyway and setting this to True avoids a lot of pointless manual
|
||||||
|
operations.
|
||||||
|
|
||||||
|
sign:
|
||||||
|
|
||||||
|
Sign the mails sent to GPG recipients with the lists key. Enabled by
|
||||||
|
default as this is the recommended way to send GPG mail. If disabled then
|
||||||
|
the public key of the list is not part of the welcome message which is
|
||||||
|
sent to new recipients.
|
||||||
|
|
||||||
|
The mailing lists collection:
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
lists:
|
||||||
|
listname:
|
||||||
|
...
|
||||||
|
listname:
|
||||||
|
|
||||||
|
lists:
|
||||||
|
|
||||||
|
The opening of the lists map.
|
||||||
|
|
||||||
|
The list base configuration:
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The list base configuration for each list consists of the following items:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
listname:
|
||||||
|
enabled: True
|
||||||
|
moderated: True
|
||||||
|
archive:
|
||||||
|
...
|
||||||
|
listaccount:
|
||||||
|
...
|
||||||
|
admins:
|
||||||
|
...
|
||||||
|
|
||||||
|
The list base items:
|
||||||
|
""""""""""""""""""""
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
listname:
|
||||||
|
enabled: True
|
||||||
|
moderated: True
|
||||||
|
|
||||||
|
enabled:
|
||||||
|
|
||||||
|
If False, the list configuration is disabled. No mail is delivered to this
|
||||||
|
list. If True, the list is enabled.
|
||||||
|
|
||||||
|
moderated:
|
||||||
|
|
||||||
|
Optional item to set the list to moderated. It True only subscribers
|
||||||
|
are allowed to post to the list either from their subscription address
|
||||||
|
or from one of the optional alias mail addresses which are associated
|
||||||
|
with a subscriber. Mails from non-subscribers are not delivered to the
|
||||||
|
list, they are delivered to the list administrator
|
||||||
|
|
||||||
|
The archive section:
|
||||||
|
""""""""""""""""""""
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
archive:
|
||||||
|
use_maildir: True
|
||||||
|
archive_incoming: True
|
||||||
|
archive_plain: False
|
||||||
|
|
||||||
|
use_maildir:
|
||||||
|
|
||||||
|
If True, maildir is the storage format for enabled archives. If False,
|
||||||
|
mbox is used.
|
||||||
|
|
||||||
|
archive_incoming:
|
||||||
|
|
||||||
|
If True archive the incoming encrypted mails in the selected storage
|
||||||
|
format. The maildir folder name is archive_incoming/. The mbox name is
|
||||||
|
archive_incoming.mbox. These files/directories are located in the per
|
||||||
|
mailing list configuration/work directory.
|
||||||
|
|
||||||
|
archive_plain:
|
||||||
|
|
||||||
|
If True archive the decrypted mails in the selected storage format. The
|
||||||
|
mails are archived in two stores:
|
||||||
|
|
||||||
|
- archive_admin[.mbox] for mails which are directed to the list admins
|
||||||
|
either directly or through bounce catching, moderation etc.
|
||||||
|
|
||||||
|
- archive_list[.mbox] for mails which are delivered to the list
|
||||||
|
|
||||||
|
The list account section:
|
||||||
|
"""""""""""""""""""""""""
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
listaccount:
|
||||||
|
list@mail.domain:
|
||||||
|
name: Clear text name
|
||||||
|
fingerprint: 40CHARACTERFINGERPRINT
|
||||||
|
|
||||||
|
The list account's e-mail address is the key item for the name and
|
||||||
|
fingerprint options.
|
||||||
|
|
||||||
|
name:
|
||||||
|
|
||||||
|
A clear text name for the list, e.g. incident-17 or whatever sensible
|
||||||
|
name is selected. This name is used in From mangling when a list post
|
||||||
|
is sent to the subscribers:
|
||||||
|
|
||||||
|
incident-17 for Joe Poster <list@mail.domain>
|
||||||
|
|
||||||
|
From rewriting is used to ensure that replies go only to the list and
|
||||||
|
not to some other place. The Reply-To field could be used as well but
|
||||||
|
that is not correctly handled by mail clients and users can force
|
||||||
|
reply to all nevertheless.
|
||||||
|
|
||||||
|
fingerprint:
|
||||||
|
|
||||||
|
The full 40 character PGP fingerprint of the list key.
|
||||||
|
|
||||||
|
.. _list_admin_section:
|
||||||
|
|
||||||
|
The list administrators section:
|
||||||
|
""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
admins:
|
||||||
|
admin1@some.domain:
|
||||||
|
name: Clear text name
|
||||||
|
fingerprint: 40CHARACTERFINGERPRINT
|
||||||
|
enabled: True
|
||||||
|
use_smime: False
|
||||||
|
gpg_plain: False
|
||||||
|
admin2@other.domain:
|
||||||
|
|
||||||
|
name:
|
||||||
|
|
||||||
|
The real user name. Mandatory field
|
||||||
|
|
||||||
|
fingerprint:
|
||||||
|
|
||||||
|
The full 40 character PGP fingerprint of the administrators
|
||||||
|
key. Mandatory if the use_smime option is not set.
|
||||||
|
|
||||||
|
enabled:
|
||||||
|
|
||||||
|
Switch to enable/disable the account. Mandatory item.
|
||||||
|
|
||||||
|
use_smime:
|
||||||
|
|
||||||
|
Send S/MIME encrypted mail to the admin if True. Otherwise use
|
||||||
|
PGP. Optional, defaults to False.
|
||||||
|
|
||||||
|
gpg_plain:
|
||||||
|
|
||||||
|
If False send mail in the application/pgp-encrypted format. If True
|
||||||
|
use the plain/text embedded PGP variant if possible. The latter does
|
||||||
|
not work for mails with attachments but for normal plain/text
|
||||||
|
conversation this can be requested by a recipient because that's
|
||||||
|
better supported in some mail clients. Optional, defaults to False.
|
||||||
|
|
||||||
|
List configuration items
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The structure of a list specific configuration file is::
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
subscribers:
|
||||||
|
subscriber1@some.domain:
|
||||||
|
...
|
||||||
|
subscriberN@other.domain:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
The configuration of the subscribers is identical to the configuration of
|
||||||
|
:ref:`the list administrators section <list_admin_section>` above, but it
|
||||||
|
allows one additional field:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
subscribers:
|
||||||
|
subscriber1@some.domain:
|
||||||
|
...
|
||||||
|
aliases:
|
||||||
|
- subscr1@some.domain
|
||||||
|
- subscriber1@other.domain
|
||||||
|
|
||||||
|
aliases:
|
||||||
|
|
||||||
|
The optional aliases item is a list of alias email addresses for a
|
||||||
|
subscriber. List mail is always delivered to the subscriber e-mail
|
||||||
|
address, but people have often several e-mail addresses covered by
|
||||||
|
the same PGP key and post from various addresses. If the list is
|
||||||
|
moderated, then the aliases allow posting for subscribers from their
|
||||||
|
registered alias addresses. If moderation is disabled the alias list
|
||||||
|
is not used at all.
|
||||||
|
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
:manpage:`remail_daemon(1)`
|
||||||
|
:manpage:`remail_chkcfg(1)`
|
||||||
|
|
97
Documentation/operation.rst
Normal file
97
Documentation/operation.rst
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
.. SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
.. _remail_operation:
|
||||||
|
|
||||||
|
remail operation related information
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Mails to list administrators
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
remail sends mail to the list administrators in case of failures or email
|
||||||
|
related issues. The mails are either incoming mails or mails composed from
|
||||||
|
the template_admin file.
|
||||||
|
|
||||||
|
The mail subject is marked with a prefix depending on the nature of the
|
||||||
|
issue:
|
||||||
|
|
||||||
|
- [MODERATED]
|
||||||
|
|
||||||
|
The mail which is sent to the moderator is an incoming list mail from a
|
||||||
|
non-subscribed sender. Subject and content are kept intact. The mail is
|
||||||
|
sent encrypted.
|
||||||
|
|
||||||
|
- [AUTOREPLY]
|
||||||
|
|
||||||
|
Auto-replies like out of office notifications are catched and forwarded
|
||||||
|
to the list administrator. There is no policy to disable subscribers
|
||||||
|
built in. This has to be handled by the administrator.
|
||||||
|
|
||||||
|
- [TEMPFAIL]
|
||||||
|
|
||||||
|
Temporary delivery failures which are bounced back from a MTA. The
|
||||||
|
affected subscriber account is not disabled. This is informational.
|
||||||
|
|
||||||
|
- [FROZEN]
|
||||||
|
|
||||||
|
A permanent delivery failure or a encryption problem happened.
|
||||||
|
|
||||||
|
The affected subscriber account is frozen due to that. See
|
||||||
|
:ref:`subscriber_status` below.
|
||||||
|
|
||||||
|
- Subject taken from the template_admin file
|
||||||
|
|
||||||
|
Mails which are composed from the template file contain internal error
|
||||||
|
information about issues which the mail processing triggered. Likely
|
||||||
|
causes are encryption/decryption failures.
|
||||||
|
|
||||||
|
Fatal issues in the list daemon like failures to connect to the local host
|
||||||
|
SMTP daemon, file system errors or bugs in the list code which trigger an
|
||||||
|
exception are not sent to the administrators of a mailing list. These
|
||||||
|
issues are reported verbosely in syslog and the base administrator needs to
|
||||||
|
take care of them.
|
||||||
|
|
||||||
|
.. _subscriber_status:
|
||||||
|
|
||||||
|
Subscriber status
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
remail has subscriber state tracking for each mailing list. The tracking
|
||||||
|
status is maintained in the file tracking.yaml which is managed by remail
|
||||||
|
in the mailing list specific directory.
|
||||||
|
|
||||||
|
The states are stored in a subscriber email-address mapping. The possible
|
||||||
|
states are:
|
||||||
|
|
||||||
|
- DISABLED
|
||||||
|
|
||||||
|
The subscriber is disabled in the configuration file.
|
||||||
|
|
||||||
|
- REGISTERED
|
||||||
|
|
||||||
|
The subscriber is enabled in the configuration file, but no welcome
|
||||||
|
mail has been sent to the subscriber.
|
||||||
|
|
||||||
|
- ENABLED
|
||||||
|
|
||||||
|
The subscriber is enabled in the configuration file. The welcome
|
||||||
|
mail has been sent to the subscriber.
|
||||||
|
|
||||||
|
- FROZEN
|
||||||
|
|
||||||
|
The subscriber is enabled in the configuration file, but the account is
|
||||||
|
frozen due to a permanent delivery failure or an internal encryption
|
||||||
|
problem.
|
||||||
|
|
||||||
|
Unfreeze
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
To unfreeze a subscriber the subscriber account has to be disabled in the
|
||||||
|
list configuration file and the list daemon configuration has to be
|
||||||
|
reloaded. After this the subscriber state is set to DISABLED. Re-enabling
|
||||||
|
the subscriber in the configuration file and reloading the daemon
|
||||||
|
configuration re-enables the account.
|
||||||
|
|
||||||
|
This is a bit tedious and could be handled by a command mail which is sent
|
||||||
|
from the list administrator to the list, but that's not yet implemented.
|
||||||
|
|
921
Documentation/remail.svg
Normal file
921
Documentation/remail.svg
Normal file
|
@ -0,0 +1,921 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:ooo="http://xml.openoffice.org/svg/export"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.2"
|
||||||
|
width="220.50999mm"
|
||||||
|
height="120.78222mm"
|
||||||
|
viewBox="0 0 22050.999 12078.222"
|
||||||
|
preserveAspectRatio="xMidYMid"
|
||||||
|
xml:space="preserve"
|
||||||
|
id="svg1624"
|
||||||
|
sodipodi:docname="remail.svg"
|
||||||
|
style="fill-rule:evenodd;stroke-width:28.22200012;stroke-linejoin:round"
|
||||||
|
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
|
||||||
|
id="metadata1628"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1027"
|
||||||
|
id="namedview1626"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.0570734"
|
||||||
|
inkscape:cx="415.74803"
|
||||||
|
inkscape:cy="285.88766"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1624"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0" />
|
||||||
|
<defs
|
||||||
|
class="ClipPathGroup"
|
||||||
|
id="defs1265">
|
||||||
|
<clipPath
|
||||||
|
id="presentation_clip_path"
|
||||||
|
clipPathUnits="userSpaceOnUse">
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="29700"
|
||||||
|
height="21000"
|
||||||
|
id="rect1259" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="presentation_clip_path_shrink"
|
||||||
|
clipPathUnits="userSpaceOnUse">
|
||||||
|
<rect
|
||||||
|
x="29"
|
||||||
|
y="21"
|
||||||
|
width="29641"
|
||||||
|
height="20958"
|
||||||
|
id="rect1262" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<defs
|
||||||
|
id="defs1332">
|
||||||
|
<font
|
||||||
|
id="EmbeddedFont_1"
|
||||||
|
horiz-adv-x="2048"
|
||||||
|
horiz-origin-x="0"
|
||||||
|
horiz-origin-y="0"
|
||||||
|
vert-origin-x="45"
|
||||||
|
vert-origin-y="90"
|
||||||
|
vert-adv-y="90">
|
||||||
|
<font-face
|
||||||
|
font-family="Liberation Sans embedded"
|
||||||
|
units-per-em="2048"
|
||||||
|
font-weight="normal"
|
||||||
|
font-style="normal"
|
||||||
|
ascent="1852"
|
||||||
|
descent="423"
|
||||||
|
id="font-face1267" />
|
||||||
|
<missing-glyph
|
||||||
|
horiz-adv-x="2048"
|
||||||
|
d="M 0,0 L 2047,0 2047,2047 0,2047 0,0 Z"
|
||||||
|
id="missing-glyph1269" />
|
||||||
|
<glyph
|
||||||
|
unicode="v"
|
||||||
|
horiz-adv-x="1033"
|
||||||
|
d="M 613,0 L 400,0 7,1082 199,1082 437,378 C 446,351 469,272 506,141 L 541,258 580,376 826,1082 1017,1082 613,0 Z"
|
||||||
|
id="glyph1271" />
|
||||||
|
<glyph
|
||||||
|
unicode="u"
|
||||||
|
horiz-adv-x="874"
|
||||||
|
d="M 314,1082 L 314,396 C 314,325 321,269 335,230 349,191 371,162 402,145 433,128 478,119 537,119 624,119 692,149 742,208 792,267 817,350 817,455 L 817,1082 997,1082 997,231 C 997,105 999,28 1003,0 L 833,0 C 832,3 832,12 831,27 830,42 830,59 829,78 828,97 826,132 825,185 L 822,185 C 781,110 733,58 679,27 624,-4 557,-20 476,-20 357,-20 271,10 216,69 161,128 133,225 133,361 L 133,1082 314,1082 Z"
|
||||||
|
id="glyph1273" />
|
||||||
|
<glyph
|
||||||
|
unicode="t"
|
||||||
|
horiz-adv-x="531"
|
||||||
|
d="M 554,8 C 495,-8 434,-16 372,-16 228,-16 156,66 156,229 L 156,951 31,951 31,1082 163,1082 216,1324 336,1324 336,1082 536,1082 536,951 336,951 336,268 C 336,216 345,180 362,159 379,138 408,127 450,127 474,127 509,132 554,141 L 554,8 Z"
|
||||||
|
id="glyph1275" />
|
||||||
|
<glyph
|
||||||
|
unicode="s"
|
||||||
|
horiz-adv-x="901"
|
||||||
|
d="M 950,299 C 950,197 912,118 835,63 758,8 650,-20 511,-20 376,-20 273,2 200,47 127,91 79,160 57,254 L 216,285 C 231,227 263,185 311,158 359,131 426,117 511,117 602,117 669,131 712,159 754,187 775,229 775,285 775,328 760,362 731,389 702,416 654,438 589,455 L 460,489 C 357,516 283,542 240,568 196,593 162,624 137,661 112,698 100,743 100,796 100,895 135,970 206,1022 276,1073 378,1099 513,1099 632,1099 727,1078 798,1036 868,994 912,927 931,834 L 769,814 C 759,862 732,899 689,925 645,950 586,963 513,963 432,963 372,951 333,926 294,901 275,864 275,814 275,783 283,758 299,738 315,718 339,701 370,687 401,673 467,654 568,629 663,605 732,583 774,563 816,542 849,520 874,495 898,470 917,442 930,410 943,377 950,340 950,299 Z"
|
||||||
|
id="glyph1277" />
|
||||||
|
<glyph
|
||||||
|
unicode="r"
|
||||||
|
horiz-adv-x="530"
|
||||||
|
d="M 142,0 L 142,830 C 142,906 140,990 136,1082 L 306,1082 C 311,959 314,886 314,861 L 318,861 C 347,954 380,1017 417,1051 454,1085 507,1102 575,1102 599,1102 623,1099 648,1092 L 648,927 C 624,934 592,937 552,937 477,937 420,905 381,841 342,776 322,684 322,564 L 322,0 142,0 Z"
|
||||||
|
id="glyph1279" />
|
||||||
|
<glyph
|
||||||
|
unicode="o"
|
||||||
|
horiz-adv-x="980"
|
||||||
|
d="M 1053,542 C 1053,353 1011,212 928,119 845,26 724,-20 565,-20 407,-20 288,28 207,125 126,221 86,360 86,542 86,915 248,1102 571,1102 736,1102 858,1057 936,966 1014,875 1053,733 1053,542 Z M 864,542 C 864,691 842,800 798,868 753,935 679,969 574,969 469,969 393,935 346,866 299,797 275,689 275,542 275,399 298,292 345,221 391,149 464,113 563,113 671,113 748,148 795,217 841,286 864,395 864,542 Z"
|
||||||
|
id="glyph1281" />
|
||||||
|
<glyph
|
||||||
|
unicode="n"
|
||||||
|
horiz-adv-x="874"
|
||||||
|
d="M 825,0 L 825,686 C 825,757 818,813 804,852 790,891 768,920 737,937 706,954 661,963 602,963 515,963 447,933 397,874 347,815 322,732 322,627 L 322,0 142,0 142,851 C 142,977 140,1054 136,1082 L 306,1082 C 307,1079 307,1070 308,1055 309,1040 310,1024 311,1005 312,986 313,950 314,897 L 317,897 C 358,972 406,1025 461,1056 515,1087 582,1102 663,1102 782,1102 869,1073 924,1014 979,955 1006,857 1006,721 L 1006,0 825,0 Z"
|
||||||
|
id="glyph1283" />
|
||||||
|
<glyph
|
||||||
|
unicode="m"
|
||||||
|
horiz-adv-x="1457"
|
||||||
|
d="M 768,0 L 768,686 C 768,791 754,863 725,903 696,943 645,963 570,963 493,963 433,934 388,875 343,816 321,734 321,627 L 321,0 142,0 142,851 C 142,977 140,1054 136,1082 L 306,1082 C 307,1079 307,1070 308,1055 309,1040 310,1024 311,1005 312,986 313,950 314,897 L 317,897 C 356,974 400,1027 450,1057 500,1087 561,1102 633,1102 715,1102 780,1086 828,1053 875,1020 908,968 927,897 L 930,897 C 967,970 1013,1022 1066,1054 1119,1086 1183,1102 1258,1102 1367,1102 1447,1072 1497,1013 1546,954 1571,856 1571,721 L 1571,0 1393,0 1393,686 C 1393,791 1379,863 1350,903 1321,943 1270,963 1195,963 1116,963 1055,934 1012,876 968,817 946,734 946,627 L 946,0 768,0 Z"
|
||||||
|
id="glyph1285" />
|
||||||
|
<glyph
|
||||||
|
unicode="l"
|
||||||
|
horiz-adv-x="187"
|
||||||
|
d="M 138,0 L 138,1484 318,1484 318,0 138,0 Z"
|
||||||
|
id="glyph1287" />
|
||||||
|
<glyph
|
||||||
|
unicode="i"
|
||||||
|
horiz-adv-x="187"
|
||||||
|
d="M 137,1312 L 137,1484 317,1484 317,1312 137,1312 Z M 137,0 L 137,1082 317,1082 317,0 137,0 Z"
|
||||||
|
id="glyph1289" />
|
||||||
|
<glyph
|
||||||
|
unicode="g"
|
||||||
|
horiz-adv-x="927"
|
||||||
|
d="M 548,-425 C 430,-425 336,-402 266,-356 196,-309 151,-243 131,-158 L 312,-132 C 324,-182 351,-220 392,-248 433,-274 486,-288 553,-288 732,-288 822,-183 822,27 L 822,201 820,201 C 786,132 739,80 680,45 621,10 551,-8 472,-8 339,-8 242,36 180,124 117,212 86,350 86,539 86,730 120,872 187,963 254,1054 355,1099 492,1099 569,1099 635,1082 692,1047 748,1012 791,962 822,897 L 824,897 C 824,917 825,952 828,1001 831,1050 833,1077 836,1082 L 1007,1082 C 1003,1046 1001,971 1001,858 L 1001,31 C 1001,-273 850,-425 548,-425 Z M 822,541 C 822,629 810,705 786,769 762,832 728,881 685,915 641,948 591,965 536,965 444,965 377,932 335,865 293,798 272,690 272,541 272,393 292,287 331,222 370,157 438,125 533,125 590,125 640,142 684,175 728,208 762,256 786,319 810,381 822,455 822,541 Z"
|
||||||
|
id="glyph1291" />
|
||||||
|
<glyph
|
||||||
|
unicode="e"
|
||||||
|
horiz-adv-x="980"
|
||||||
|
d="M 276,503 C 276,379 302,283 353,216 404,149 479,115 578,115 656,115 719,131 766,162 813,193 844,233 861,281 L 1019,236 C 954,65 807,-20 578,-20 418,-20 296,28 213,123 129,218 87,360 87,548 87,727 129,864 213,959 296,1054 416,1102 571,1102 889,1102 1048,910 1048,527 L 1048,503 276,503 Z M 862,641 C 852,755 823,838 775,891 727,943 658,969 568,969 481,969 412,940 361,882 310,823 282,743 278,641 L 862,641 Z"
|
||||||
|
id="glyph1293" />
|
||||||
|
<glyph
|
||||||
|
unicode="d"
|
||||||
|
horiz-adv-x="927"
|
||||||
|
d="M 821,174 C 788,105 744,55 689,25 634,-5 565,-20 484,-20 347,-20 247,26 183,118 118,210 86,349 86,536 86,913 219,1102 484,1102 566,1102 634,1087 689,1057 744,1027 788,979 821,914 L 823,914 821,1035 821,1484 1001,1484 1001,223 C 1001,110 1003,36 1007,0 L 835,0 C 833,11 831,35 829,74 826,113 825,146 825,174 L 821,174 Z M 275,542 C 275,391 295,282 335,217 375,152 440,119 530,119 632,119 706,154 752,225 798,296 821,405 821,554 821,697 798,802 752,869 706,936 633,969 532,969 441,969 376,936 336,869 295,802 275,693 275,542 Z"
|
||||||
|
id="glyph1295" />
|
||||||
|
<glyph
|
||||||
|
unicode="c"
|
||||||
|
horiz-adv-x="901"
|
||||||
|
d="M 275,546 C 275,402 298,295 343,226 388,157 457,122 548,122 612,122 666,139 709,174 752,209 778,262 788,334 L 970,322 C 956,218 912,135 837,73 762,11 668,-20 553,-20 402,-20 286,28 207,124 127,219 87,359 87,542 87,724 127,863 207,959 287,1054 402,1102 551,1102 662,1102 754,1073 827,1016 900,959 945,880 964,779 L 779,765 C 770,825 746,873 708,908 670,943 616,961 546,961 451,961 382,929 339,866 296,803 275,696 275,546 Z"
|
||||||
|
id="glyph1297" />
|
||||||
|
<glyph
|
||||||
|
unicode="b"
|
||||||
|
horiz-adv-x="953"
|
||||||
|
d="M 1053,546 C 1053,169 920,-20 655,-20 573,-20 505,-5 451,25 396,54 352,102 318,168 L 316,168 C 316,147 315,116 312,74 309,31 307,7 306,0 L 132,0 C 136,36 138,110 138,223 L 138,1484 318,1484 318,1061 C 318,1018 317,967 314,908 L 318,908 C 351,977 396,1027 451,1057 506,1087 574,1102 655,1102 792,1102 892,1056 957,964 1021,872 1053,733 1053,546 Z M 864,540 C 864,691 844,800 804,865 764,930 699,963 609,963 508,963 434,928 388,859 341,790 318,680 318,529 318,387 341,282 386,215 431,147 505,113 607,113 698,113 763,147 804,214 844,281 864,389 864,540 Z"
|
||||||
|
id="glyph1299" />
|
||||||
|
<glyph
|
||||||
|
unicode="a"
|
||||||
|
horiz-adv-x="1060"
|
||||||
|
d="M 414,-20 C 305,-20 224,9 169,66 114,123 87,202 87,302 87,414 124,500 198,560 271,620 390,652 554,656 L 797,660 797,719 C 797,807 778,870 741,908 704,946 645,965 565,965 484,965 426,951 389,924 352,897 330,853 323,793 L 135,810 C 166,1005 310,1102 569,1102 705,1102 807,1071 876,1009 945,946 979,856 979,738 L 979,272 C 979,219 986,179 1000,152 1014,125 1041,111 1080,111 1097,111 1117,113 1139,118 L 1139,6 C 1094,-5 1047,-10 1000,-10 933,-10 885,8 855,43 824,78 807,132 803,207 L 797,207 C 751,124 698,66 637,32 576,-3 501,-20 414,-20 Z M 455,115 C 521,115 580,130 631,160 682,190 723,231 753,284 782,336 797,390 797,445 L 797,534 600,530 C 515,529 451,520 408,504 364,488 330,463 307,430 284,397 272,353 272,299 272,240 288,195 320,163 351,131 396,115 455,115 Z"
|
||||||
|
id="glyph1301" />
|
||||||
|
<glyph
|
||||||
|
unicode="W"
|
||||||
|
horiz-adv-x="1932"
|
||||||
|
d="M 1511,0 L 1283,0 1039,895 C 1023,951 1000,1051 969,1196 952,1119 937,1054 925,1002 913,950 822,616 652,0 L 424,0 9,1409 208,1409 461,514 C 491,402 519,287 544,168 560,241 579,321 600,408 621,495 713,828 877,1409 L 1060,1409 1305,532 C 1342,389 1372,267 1393,168 L 1402,203 C 1420,280 1435,342 1446,391 1457,439 1551,778 1727,1409 L 1926,1409 1511,0 Z"
|
||||||
|
id="glyph1303" />
|
||||||
|
<glyph
|
||||||
|
unicode="T"
|
||||||
|
horiz-adv-x="1192"
|
||||||
|
d="M 720,1253 L 720,0 530,0 530,1253 46,1253 46,1409 1204,1409 1204,1253 720,1253 Z"
|
||||||
|
id="glyph1305" />
|
||||||
|
<glyph
|
||||||
|
unicode="S"
|
||||||
|
horiz-adv-x="1192"
|
||||||
|
d="M 1272,389 C 1272,259 1221,158 1120,87 1018,16 875,-20 690,-20 347,-20 148,99 93,338 L 278,375 C 299,290 345,228 414,189 483,149 578,129 697,129 820,129 916,150 983,193 1050,235 1083,297 1083,379 1083,425 1073,462 1052,491 1031,520 1001,543 963,562 925,581 880,596 827,609 774,622 716,635 652,650 541,675 456,699 399,724 341,749 295,776 262,807 229,837 203,872 186,913 168,954 159,1000 159,1053 159,1174 205,1267 298,1332 390,1397 522,1430 694,1430 854,1430 976,1406 1061,1357 1146,1308 1205,1224 1239,1106 L 1051,1073 C 1030,1148 991,1202 933,1236 875,1269 795,1286 692,1286 579,1286 493,1267 434,1230 375,1193 345,1137 345,1063 345,1020 357,984 380,956 403,927 436,903 479,884 522,864 609,840 738,811 781,801 825,791 868,781 911,770 952,758 991,744 1030,729 1067,712 1102,693 1136,674 1166,650 1191,622 1216,594 1236,561 1251,523 1265,485 1272,440 1272,389 Z"
|
||||||
|
id="glyph1307" />
|
||||||
|
<glyph
|
||||||
|
unicode="R"
|
||||||
|
horiz-adv-x="1244"
|
||||||
|
d="M 1164,0 L 798,585 359,585 359,0 168,0 168,1409 831,1409 C 990,1409 1112,1374 1199,1303 1285,1232 1328,1133 1328,1006 1328,901 1298,813 1237,742 1176,671 1091,626 984,607 L 1384,0 1164,0 Z M 1136,1004 C 1136,1086 1108,1149 1053,1192 997,1235 917,1256 812,1256 L 359,1256 359,736 820,736 C 921,736 999,760 1054,807 1109,854 1136,919 1136,1004 Z"
|
||||||
|
id="glyph1309" />
|
||||||
|
<glyph
|
||||||
|
unicode="P"
|
||||||
|
horiz-adv-x="1112"
|
||||||
|
d="M 1258,985 C 1258,852 1215,746 1128,667 1041,588 922,549 773,549 L 359,549 359,0 168,0 168,1409 761,1409 C 919,1409 1041,1372 1128,1298 1215,1224 1258,1120 1258,985 Z M 1066,983 C 1066,1165 957,1256 738,1256 L 359,1256 359,700 746,700 C 959,700 1066,794 1066,983 Z"
|
||||||
|
id="glyph1311" />
|
||||||
|
<glyph
|
||||||
|
unicode="M"
|
||||||
|
horiz-adv-x="1377"
|
||||||
|
d="M 1366,0 L 1366,940 C 1366,1044 1369,1144 1375,1240 1342,1121 1313,1027 1287,960 L 923,0 789,0 420,960 364,1130 331,1240 334,1129 338,940 338,0 168,0 168,1409 419,1409 794,432 C 807,393 820,351 833,306 845,261 853,228 857,208 862,235 874,275 891,330 908,384 919,418 925,432 L 1293,1409 1538,1409 1538,0 1366,0 Z"
|
||||||
|
id="glyph1313" />
|
||||||
|
<glyph
|
||||||
|
unicode="L"
|
||||||
|
horiz-adv-x="927"
|
||||||
|
d="M 168,0 L 168,1409 359,1409 359,156 1071,156 1071,0 168,0 Z"
|
||||||
|
id="glyph1315" />
|
||||||
|
<glyph
|
||||||
|
unicode="I"
|
||||||
|
horiz-adv-x="213"
|
||||||
|
d="M 189,0 L 189,1409 380,1409 380,0 189,0 Z"
|
||||||
|
id="glyph1317" />
|
||||||
|
<glyph
|
||||||
|
unicode="F"
|
||||||
|
horiz-adv-x="1006"
|
||||||
|
d="M 359,1253 L 359,729 1145,729 1145,571 359,571 359,0 168,0 168,1409 1169,1409 1169,1253 359,1253 Z"
|
||||||
|
id="glyph1319" />
|
||||||
|
<glyph
|
||||||
|
unicode="E"
|
||||||
|
horiz-adv-x="1138"
|
||||||
|
d="M 168,0 L 168,1409 1237,1409 1237,1253 359,1253 359,801 1177,801 1177,647 359,647 359,156 1278,156 1278,0 168,0 Z"
|
||||||
|
id="glyph1321" />
|
||||||
|
<glyph
|
||||||
|
unicode="A"
|
||||||
|
horiz-adv-x="1377"
|
||||||
|
d="M 1167,0 L 1006,412 364,412 202,0 4,0 579,1409 796,1409 1362,0 1167,0 Z M 685,1265 L 676,1237 C 659,1182 635,1111 602,1024 L 422,561 949,561 768,1026 C 749,1072 731,1124 712,1182 L 685,1265 Z"
|
||||||
|
id="glyph1323" />
|
||||||
|
<glyph
|
||||||
|
unicode=")"
|
||||||
|
horiz-adv-x="557"
|
||||||
|
d="M 555,528 C 555,335 525,162 465,9 404,-144 311,-289 186,-424 L 12,-424 C 137,-284 229,-136 287,19 345,174 374,344 374,530 374,716 345,887 287,1042 228,1197 137,1345 12,1484 L 186,1484 C 312,1348 405,1203 465,1050 525,896 555,723 555,532 L 555,528 Z"
|
||||||
|
id="glyph1325" />
|
||||||
|
<glyph
|
||||||
|
unicode="("
|
||||||
|
horiz-adv-x="583"
|
||||||
|
d="M 127,532 C 127,725 157,898 218,1051 278,1204 371,1349 496,1484 L 670,1484 C 545,1345 454,1198 396,1042 337,886 308,715 308,530 308,345 337,175 395,20 452,-135 544,-283 670,-424 L 496,-424 C 370,-288 277,-143 217,11 157,164 127,337 127,528 L 127,532 Z"
|
||||||
|
id="glyph1327" />
|
||||||
|
<glyph
|
||||||
|
unicode=" "
|
||||||
|
horiz-adv-x="556"
|
||||||
|
id="glyph1329" />
|
||||||
|
</font>
|
||||||
|
</defs>
|
||||||
|
<defs
|
||||||
|
class="TextShapeIndex"
|
||||||
|
id="defs1336">
|
||||||
|
<g
|
||||||
|
ooo:slide="id1"
|
||||||
|
ooo:id-list="id3 id4 id5 id6 id7 id8 id9 id10 id11 id12 id13 id14 id15"
|
||||||
|
id="g1334" />
|
||||||
|
</defs>
|
||||||
|
<defs
|
||||||
|
class="EmbeddedBulletChars"
|
||||||
|
id="defs1368">
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-57356"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M 580,1141 1163,571 580,0 -4,571 Z"
|
||||||
|
id="path1338"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-57354"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M 8,1128 H 1137 V 0 H 8 Z"
|
||||||
|
id="path1341"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-10146"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M 174,0 602,739 174,1481 1456,739 Z M 1358,739 309,1346 659,739 Z"
|
||||||
|
id="path1344"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-10132"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M 2015,739 1276,0 H 717 l 543,543 H 174 v 393 h 1086 l -543,545 h 557 z"
|
||||||
|
id="path1347"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-10007"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="m 0,-2 c -7,16 -16,29 -25,39 l 381,530 c -94,256 -141,385 -141,387 0,25 13,38 40,38 9,0 21,-2 34,-5 21,4 42,12 65,25 l 27,-13 111,-251 280,301 64,-25 24,25 c 21,-10 41,-24 62,-43 C 886,937 835,863 770,784 769,783 710,716 594,584 L 774,223 c 0,-27 -21,-55 -63,-84 l 16,-20 C 717,90 699,76 672,76 641,76 570,178 457,381 L 164,-76 c -22,-34 -53,-51 -92,-51 -42,0 -63,17 -64,51 -7,9 -10,24 -10,44 0,9 1,19 2,30 z"
|
||||||
|
id="path1350"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-10004"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M 285,-33 C 182,-33 111,30 74,156 52,228 41,333 41,471 c 0,78 14,145 41,201 34,71 87,106 158,106 53,0 88,-31 106,-94 l 23,-176 c 8,-64 28,-97 59,-98 l 735,706 c 11,11 33,17 66,17 42,0 63,-15 63,-46 V 965 c 0,-36 -10,-64 -30,-84 L 442,47 C 390,-6 338,-33 285,-33 Z"
|
||||||
|
id="path1353"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-9679"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M 813,0 C 632,0 489,54 383,161 276,268 223,411 223,592 c 0,181 53,324 160,431 106,107 249,161 430,161 179,0 323,-54 432,-161 108,-107 162,-251 162,-431 0,-180 -54,-324 -162,-431 C 1136,54 992,0 813,0 Z"
|
||||||
|
id="path1356"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-8226"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="m 346,457 c -73,0 -137,26 -191,78 -54,51 -81,114 -81,188 0,73 27,136 81,188 54,52 118,78 191,78 73,0 134,-26 185,-79 51,-51 77,-114 77,-187 0,-75 -25,-137 -76,-188 -50,-52 -112,-78 -186,-78 z"
|
||||||
|
id="path1359"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-8211"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="M -4,459 H 1135 V 606 H -4 Z"
|
||||||
|
id="path1362"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="bullet-char-template-61548"
|
||||||
|
transform="matrix(4.8828125e-4,0,0,-4.8828125e-4,0,0)">
|
||||||
|
<path
|
||||||
|
d="m 173,740 c 0,163 58,303 173,419 116,115 255,173 419,173 163,0 302,-58 418,-173 116,-116 174,-256 174,-419 0,-163 -58,-303 -174,-418 C 1067,206 928,148 765,148 601,148 462,206 346,322 231,437 173,577 173,740 Z"
|
||||||
|
id="path1365"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<defs
|
||||||
|
class="TextEmbeddedBitmaps"
|
||||||
|
id="defs1370" />
|
||||||
|
<g
|
||||||
|
id="g1375"
|
||||||
|
transform="translate(-3850,-5985.889)">
|
||||||
|
<g
|
||||||
|
id="id2"
|
||||||
|
class="Master_Slide">
|
||||||
|
<g
|
||||||
|
id="bg-id2"
|
||||||
|
class="Background" />
|
||||||
|
<g
|
||||||
|
id="bo-id2"
|
||||||
|
class="BackgroundObjects" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="SlideGroup"
|
||||||
|
id="g1622"
|
||||||
|
transform="translate(-3850,-5985.889)">
|
||||||
|
<g
|
||||||
|
id="g1620">
|
||||||
|
<g
|
||||||
|
id="container-id1">
|
||||||
|
<g
|
||||||
|
id="id1"
|
||||||
|
class="Slide"
|
||||||
|
clip-path="url(#presentation_clip_path)">
|
||||||
|
<g
|
||||||
|
class="Page"
|
||||||
|
id="g1616">
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1392">
|
||||||
|
<g
|
||||||
|
id="id3">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="20749"
|
||||||
|
y="13499"
|
||||||
|
width="3903"
|
||||||
|
height="2903"
|
||||||
|
id="rect1377"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 22700,16400 h -1950 v -2900 h 3900 v 2900 z"
|
||||||
|
id="path1379"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 22700,16400 h -1950 v -2900 h 3900 v 2900 z"
|
||||||
|
id="path1381"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1389"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1387"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="21769"
|
||||||
|
y="15171"
|
||||||
|
id="tspan1385"><tspan
|
||||||
|
id="tspan1383"
|
||||||
|
style="fill:#000000;stroke:none">maildir</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1415">
|
||||||
|
<g
|
||||||
|
id="id4">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="20749"
|
||||||
|
y="8399"
|
||||||
|
width="4003"
|
||||||
|
height="3103"
|
||||||
|
id="rect1394"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 22750,11500 H 20750 V 8400 h 4000 v 3100 z"
|
||||||
|
id="path1396"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 22750,11500 H 20750 V 8400 h 4000 v 3100 z"
|
||||||
|
id="path1398"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1412"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1404"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="21889"
|
||||||
|
y="9815"
|
||||||
|
id="tspan1402"><tspan
|
||||||
|
id="tspan1400"
|
||||||
|
style="fill:#000000;stroke:none">remail</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1410"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="21607"
|
||||||
|
y="10526"
|
||||||
|
id="tspan1408"><tspan
|
||||||
|
id="tspan1406"
|
||||||
|
style="fill:#000000;stroke:none">daemon</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1444">
|
||||||
|
<g
|
||||||
|
id="id5">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="14849"
|
||||||
|
y="12399"
|
||||||
|
width="3903"
|
||||||
|
height="3103"
|
||||||
|
id="rect1417"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 16800,15500 h -1950 v -3100 h 3900 v 3100 z"
|
||||||
|
id="path1419"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 16800,15500 h -1950 v -3100 h 3900 v 3100 z"
|
||||||
|
id="path1421"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1441"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1427"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="16026"
|
||||||
|
y="13460"
|
||||||
|
id="tspan1425"><tspan
|
||||||
|
id="tspan1423"
|
||||||
|
style="fill:#000000;stroke:none">(Mail </tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1433"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="15551"
|
||||||
|
y="14171"
|
||||||
|
id="tspan1431"><tspan
|
||||||
|
id="tspan1429"
|
||||||
|
style="fill:#000000;stroke:none">Retrieval</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1439"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="15867"
|
||||||
|
y="14882"
|
||||||
|
id="tspan1437"><tspan
|
||||||
|
id="tspan1435"
|
||||||
|
style="fill:#000000;stroke:none">Agent)</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1461">
|
||||||
|
<g
|
||||||
|
id="id6">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="14849"
|
||||||
|
y="9499"
|
||||||
|
width="3903"
|
||||||
|
height="2903"
|
||||||
|
id="rect1446"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 16800,12400 H 14850 V 9500 h 3900 v 2900 z"
|
||||||
|
id="path1448"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 16800,12400 H 14850 V 9500 h 3900 v 2900 z"
|
||||||
|
id="path1450"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1458"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1456"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="15918"
|
||||||
|
y="11171"
|
||||||
|
id="tspan1454"><tspan
|
||||||
|
id="tspan1452"
|
||||||
|
style="fill:#000000;stroke:none">SMTP</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1486">
|
||||||
|
<g
|
||||||
|
id="id7">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="5800"
|
||||||
|
y="9950"
|
||||||
|
width="4101"
|
||||||
|
height="4101"
|
||||||
|
id="rect1463"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 7850,14000 H 5850 v -4000 h 4000 v 4000 z"
|
||||||
|
id="path1465"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 7850,14000 H 5850 v -4000 h 4000 v 4000 z"
|
||||||
|
id="path1467"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:100;stroke-linejoin:round" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1483"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1473"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="6989"
|
||||||
|
y="11865"
|
||||||
|
id="tspan1471"><tspan
|
||||||
|
id="tspan1469"
|
||||||
|
style="fill:#000000;stroke:none">Public</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1481"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="6301"
|
||||||
|
y="12576"
|
||||||
|
id="tspan1479"><tspan
|
||||||
|
id="tspan1475"
|
||||||
|
style="fill:#000000;stroke:none"> </tspan><tspan
|
||||||
|
id="tspan1477"
|
||||||
|
style="fill:#000000;stroke:none">Mailserver</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.LineShape"
|
||||||
|
id="g1497">
|
||||||
|
<g
|
||||||
|
id="id8">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="3850"
|
||||||
|
y="11850"
|
||||||
|
width="2001"
|
||||||
|
height="301"
|
||||||
|
id="rect1488"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 4280,12000 H 5420"
|
||||||
|
id="path1490"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<path
|
||||||
|
d="m 3850,12000 450,150 v -300 z"
|
||||||
|
id="path1492"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 5850,12000 -450,-150 v 300 z"
|
||||||
|
id="path1494"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.LineShape"
|
||||||
|
id="g1508">
|
||||||
|
<g
|
||||||
|
id="id9">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="9850"
|
||||||
|
y="11850"
|
||||||
|
width="5001"
|
||||||
|
height="301"
|
||||||
|
id="rect1499"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 10280,12000 h 4140"
|
||||||
|
id="path1501"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<path
|
||||||
|
d="m 9850,12000 450,150 v -300 z"
|
||||||
|
id="path1503"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 14850,12000 -450,-150 v 300 z"
|
||||||
|
id="path1505"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1515">
|
||||||
|
<g
|
||||||
|
id="id10">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="14800"
|
||||||
|
y="6950"
|
||||||
|
width="11101"
|
||||||
|
height="10101"
|
||||||
|
id="rect1510"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 20350,17000 H 14850 V 7000 h 11000 v 10000 z"
|
||||||
|
id="path1512"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:100;stroke-linejoin:round" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.LineShape"
|
||||||
|
id="g1524">
|
||||||
|
<g
|
||||||
|
id="id11">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="18750"
|
||||||
|
y="10350"
|
||||||
|
width="2002"
|
||||||
|
height="301"
|
||||||
|
id="rect1517"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 20750,10500 H 19180"
|
||||||
|
id="path1519"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<path
|
||||||
|
d="m 18750,10500 450,150 v -300 z"
|
||||||
|
id="path1521"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.LineShape"
|
||||||
|
id="g1533">
|
||||||
|
<g
|
||||||
|
id="id12">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="18749"
|
||||||
|
y="14350"
|
||||||
|
width="2002"
|
||||||
|
height="301"
|
||||||
|
id="rect1526"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 18750,14500 h 1570"
|
||||||
|
id="path1528"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<path
|
||||||
|
d="m 20750,14500 -450,-150 v 300 z"
|
||||||
|
id="path1530"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.LineShape"
|
||||||
|
id="g1542">
|
||||||
|
<g
|
||||||
|
id="id13">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="22579"
|
||||||
|
y="11461"
|
||||||
|
width="301"
|
||||||
|
height="2002"
|
||||||
|
id="rect1535"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="M 22729,13461 V 11891"
|
||||||
|
id="path1537"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#000000" />
|
||||||
|
<path
|
||||||
|
d="m 22729,11461 -150,450 h 300 z"
|
||||||
|
id="path1539"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#000000;stroke:none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.CustomShape"
|
||||||
|
id="g1601">
|
||||||
|
<g
|
||||||
|
id="id14">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="11999"
|
||||||
|
y="5999"
|
||||||
|
width="1003"
|
||||||
|
height="12053"
|
||||||
|
id="rect1544"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 12500,18050 h -500 V 6000 h 1000 v 12050 z"
|
||||||
|
id="path1546"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ff0000;stroke:none" />
|
||||||
|
<path
|
||||||
|
d="m 12500,18050 h -500 V 6000 h 1000 v 12050 z"
|
||||||
|
id="path1548"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#3465a4" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1598"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1554"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12305"
|
||||||
|
y="9757"
|
||||||
|
id="tspan1552"><tspan
|
||||||
|
id="tspan1550"
|
||||||
|
style="fill:#000000;stroke:none">F</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1560"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12411"
|
||||||
|
y="10468"
|
||||||
|
id="tspan1558"><tspan
|
||||||
|
id="tspan1556"
|
||||||
|
style="fill:#000000;stroke:none">I</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1566"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12271"
|
||||||
|
y="11179"
|
||||||
|
id="tspan1564"><tspan
|
||||||
|
id="tspan1562"
|
||||||
|
style="fill:#000000;stroke:none">R</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1572"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12288"
|
||||||
|
y="11890"
|
||||||
|
id="tspan1570"><tspan
|
||||||
|
id="tspan1568"
|
||||||
|
style="fill:#000000;stroke:none">E</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1578"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12199"
|
||||||
|
y="12601"
|
||||||
|
id="tspan1576"><tspan
|
||||||
|
id="tspan1574"
|
||||||
|
style="fill:#000000;stroke:none">W</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1584"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12288"
|
||||||
|
y="13312"
|
||||||
|
id="tspan1582"><tspan
|
||||||
|
id="tspan1580"
|
||||||
|
style="fill:#000000;stroke:none">A</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1590"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12324"
|
||||||
|
y="14023"
|
||||||
|
id="tspan1588"><tspan
|
||||||
|
id="tspan1586"
|
||||||
|
style="fill:#000000;stroke:none">L</tspan></tspan></tspan><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1596"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="12324"
|
||||||
|
y="14734"
|
||||||
|
id="tspan1594"><tspan
|
||||||
|
id="tspan1592"
|
||||||
|
style="fill:#000000;stroke:none">L</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
class="com.sun.star.drawing.TextShape"
|
||||||
|
id="g1614">
|
||||||
|
<g
|
||||||
|
id="id15">
|
||||||
|
<rect
|
||||||
|
class="BoundingBox"
|
||||||
|
x="17200"
|
||||||
|
y="7300"
|
||||||
|
width="5786"
|
||||||
|
height="963"
|
||||||
|
id="rect1603"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<text
|
||||||
|
class="TextShape"
|
||||||
|
id="text1611"><tspan
|
||||||
|
class="TextParagraph"
|
||||||
|
font-size="635px"
|
||||||
|
font-weight="400"
|
||||||
|
id="tspan1609"
|
||||||
|
style="font-weight:400;font-size:635px;font-family:'Liberation Sans', sans-serif"><tspan
|
||||||
|
class="TextPosition"
|
||||||
|
x="17450"
|
||||||
|
y="8001"
|
||||||
|
id="tspan1607"><tspan
|
||||||
|
id="tspan1605"
|
||||||
|
style="fill:#000000;stroke:none">Secured Listserver</tspan></tspan></tspan></text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 33 KiB |
266
LICENSES/GPL-2.0
Normal file
266
LICENSES/GPL-2.0
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
Valid-License-Identifier: GPL-2.0-only
|
||||||
|
Valid-License-Identifier: GPL-2.0-or-later
|
||||||
|
SPDX-URL: https://spdx.org/licenses/GPL-2.0-only.html
|
||||||
|
Usage-Guide:
|
||||||
|
To use this license in source code, put one of the following SPDX
|
||||||
|
tag/value pairs into a comment according to the placement
|
||||||
|
guidelines in the licensing rules documentation.
|
||||||
|
For 'GNU General Public License (GPL) version 2 only' use:
|
||||||
|
SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
For 'GNU General Public License (GPL) version 2 or any later version' use:
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
License-Text:
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share
|
||||||
|
and change it. By contrast, the GNU General Public License is intended to
|
||||||
|
guarantee your freedom to share and change free software--to make sure the
|
||||||
|
software is free for all its users. This General Public License applies to most
|
||||||
|
of the Free Software Foundation's software and to any other program whose
|
||||||
|
authors commit to using it. (Some other Free Software Foundation software is
|
||||||
|
covered by the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our
|
||||||
|
General Public Licenses are designed to make sure that you have the freedom to
|
||||||
|
distribute copies of free software (and charge for this service if you wish),
|
||||||
|
that you receive source code or can get it if you want it, that you can change
|
||||||
|
the software or use pieces of it in new free programs; and that you know you
|
||||||
|
can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny
|
||||||
|
you these rights or to ask you to surrender the rights. These restrictions
|
||||||
|
translate to certain responsibilities for you if you distribute copies of the
|
||||||
|
software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for
|
||||||
|
a fee, you must give the recipients all the rights that you have. You must make
|
||||||
|
sure that they, too, receive or can get the source code. And you must show them
|
||||||
|
these terms so they know their rights.
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2)
|
||||||
|
offer you this license which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that
|
||||||
|
everyone understands that there is no warranty for this free software. If the
|
||||||
|
software is modified by someone else and passed on, we want its recipients to
|
||||||
|
know that what they have is not the original, so that any problems introduced
|
||||||
|
by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish
|
||||||
|
to avoid the danger that redistributors of a free program will individually
|
||||||
|
obtain patent licenses, in effect making the program proprietary. To prevent
|
||||||
|
this, we have made it clear that any patent must be licensed for everyone's
|
||||||
|
free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification
|
||||||
|
follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice
|
||||||
|
placed by the copyright holder saying it may be distributed under the terms of
|
||||||
|
this General Public License. The "Program", below, refers to any such program
|
||||||
|
or work, and a "work based on the Program" means either the Program or any
|
||||||
|
derivative work under copyright law: that is to say, a work containing the
|
||||||
|
Program or a portion of it, either verbatim or with modifications and/or
|
||||||
|
translated into another language. (Hereinafter, translation is included without
|
||||||
|
limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
Activities other than copying, distribution and modification are not covered by
|
||||||
|
this License; they are outside its scope. The act of running the Program is not
|
||||||
|
restricted, and the output from the Program is covered only if its contents
|
||||||
|
constitute a work based on the Program (independent of having been made by
|
||||||
|
running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as
|
||||||
|
you receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice and
|
||||||
|
disclaimer of warranty; keep intact all the notices that refer to this License
|
||||||
|
and to the absence of any warranty; and give any other recipients of the
|
||||||
|
Program a copy of this License along with the Program.
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may
|
||||||
|
at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus
|
||||||
|
forming a work based on the Program, and copy and distribute such modifications
|
||||||
|
or work under the terms of Section 1 above, provided that you also meet all of
|
||||||
|
these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating
|
||||||
|
that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole
|
||||||
|
or in part contains or is derived from the Program or any part thereof,
|
||||||
|
to be licensed as a whole at no charge to all third parties under the
|
||||||
|
terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when
|
||||||
|
run, you must cause it, when started running for such interactive use in
|
||||||
|
the most ordinary way, to print or display an announcement including an
|
||||||
|
appropriate copyright notice and a notice that there is no warranty (or
|
||||||
|
else, saying that you provide a warranty) and that users may redistribute
|
||||||
|
the program under these conditions, and telling the user how to view a
|
||||||
|
copy of this License. (Exception: if the Program itself is interactive
|
||||||
|
but does not normally print such an announcement, your work based on the
|
||||||
|
Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable
|
||||||
|
sections of that work are not derived from the Program, and can be reasonably
|
||||||
|
considered independent and separate works in themselves, then this License, and
|
||||||
|
its terms, do not apply to those sections when you distribute them as separate
|
||||||
|
works. But when you distribute the same sections as part of a whole which is a
|
||||||
|
work based on the Program, the distribution of the whole must be on the terms
|
||||||
|
of this License, whose permissions for other licensees extend to the entire
|
||||||
|
whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your
|
||||||
|
rights to work written entirely by you; rather, the intent is to exercise the
|
||||||
|
right to control the distribution of derivative or collective works based on
|
||||||
|
the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the
|
||||||
|
Program (or with a work based on the Program) on a volume of a storage or
|
||||||
|
distribution medium does not bring the other work under the scope of this
|
||||||
|
License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under
|
||||||
|
Section 2) in object code or executable form under the terms of Sections 1 and
|
||||||
|
2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source
|
||||||
|
code, which must be distributed under the terms of Sections 1 and 2 above
|
||||||
|
on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to
|
||||||
|
give any third party, for a charge no more than your cost of physically
|
||||||
|
performing source distribution, a complete machine-readable copy of the
|
||||||
|
corresponding source code, to be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to
|
||||||
|
distribute corresponding source code. (This alternative is allowed only
|
||||||
|
for noncommercial distribution and only if you received the program in
|
||||||
|
object code or executable form with such an offer, in accord with
|
||||||
|
Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making
|
||||||
|
modifications to it. For an executable work, complete source code means all the
|
||||||
|
source code for all modules it contains, plus any associated interface
|
||||||
|
definition files, plus the scripts used to control compilation and installation
|
||||||
|
of the executable. However, as a special exception, the source code distributed
|
||||||
|
need not include anything that is normally distributed (in either source or
|
||||||
|
binary form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component itself
|
||||||
|
accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the source
|
||||||
|
code from the same place counts as distribution of the source code, even though
|
||||||
|
third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as
|
||||||
|
expressly provided under this License. Any attempt otherwise to copy, modify,
|
||||||
|
sublicense or distribute the Program is void, and will automatically terminate
|
||||||
|
your rights under this License. However, parties who have received copies, or
|
||||||
|
rights, from you under this License will not have their licenses terminated so
|
||||||
|
long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it.
|
||||||
|
However, nothing else grants you permission to modify or distribute the Program
|
||||||
|
or its derivative works. These actions are prohibited by law if you do not
|
||||||
|
accept this License. Therefore, by modifying or distributing the Program (or
|
||||||
|
any work based on the Program), you indicate your acceptance of this License to
|
||||||
|
do so, and all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program),
|
||||||
|
the recipient automatically receives a license from the original licensor to
|
||||||
|
copy, distribute or modify the Program subject to these terms and conditions.
|
||||||
|
You may not impose any further restrictions on the recipients' exercise of the
|
||||||
|
rights granted herein. You are not responsible for enforcing compliance by
|
||||||
|
third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues), conditions
|
||||||
|
are imposed on you (whether by court order, agreement or otherwise) that
|
||||||
|
contradict the conditions of this License, they do not excuse you from the
|
||||||
|
conditions of this License. If you cannot distribute so as to satisfy
|
||||||
|
simultaneously your obligations under this License and any other pertinent
|
||||||
|
obligations, then as a consequence you may not distribute the Program at all.
|
||||||
|
|
||||||
|
For example, if a patent license would not permit royalty-free redistribution
|
||||||
|
of the Program by all those who receive copies directly or indirectly through
|
||||||
|
you, then the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply and
|
||||||
|
the section as a whole is intended to apply in other circumstances.
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or
|
||||||
|
other property right claims or to contest validity of any such claims; this
|
||||||
|
section has the sole purpose of protecting the integrity of the free software
|
||||||
|
distribution system, which is implemented by public license practices. Many
|
||||||
|
people have made generous contributions to the wide range of software
|
||||||
|
distributed through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing to
|
||||||
|
distribute software through any other system and a licensee cannot impose that
|
||||||
|
choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a
|
||||||
|
consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain
|
||||||
|
countries either by patents or by copyrighted interfaces, the original
|
||||||
|
copyright holder who places the Program under this License may add an explicit
|
||||||
|
geographical distribution limitation excluding those countries, so that
|
||||||
|
distribution is permitted only in or among countries not thus excluded. In such
|
||||||
|
case, this License incorporates the limitation as if written in the body of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the
|
||||||
|
General Public License from time to time. Such new versions will be similar in
|
||||||
|
spirit to the present version, but may differ in detail to address new problems
|
||||||
|
or concerns.
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies
|
||||||
|
a version number of this License which applies to it and "any later version",
|
||||||
|
you have the option of following the terms and conditions either of that
|
||||||
|
version or of any later version published by the Free Software Foundation. If
|
||||||
|
the Program does not specify a version number of this License, you may choose
|
||||||
|
any version ever published by the Free Software Foundation.
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs
|
||||||
|
whose distribution conditions are different, write to the author to ask for
|
||||||
|
permission. For software which is copyrighted by the Free Software Foundation,
|
||||||
|
write to the Free Software Foundation; we sometimes make exceptions for this.
|
||||||
|
Our decision will be guided by the two goals of preserving the free status of
|
||||||
|
all derivatives of our free software and of promoting the sharing and reuse of
|
||||||
|
software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
|
||||||
|
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
|
||||||
|
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
|
||||||
|
PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
|
||||||
|
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||||
|
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
|
||||||
|
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
|
||||||
|
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
|
||||||
|
PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
|
||||||
|
INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
|
||||||
|
BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
|
||||||
|
OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
17
README.md
Normal file
17
README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# remail - A set of tools for crypted mailing lists
|
||||||
|
|
||||||
|
A simplistic but powerfull crypted mailing list tool set. The following
|
||||||
|
tools are available:
|
||||||
|
|
||||||
|
remail_daemon - The crypto mailing list processor
|
||||||
|
remail_chkcfg - Tool to check the configuration
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
It's recommended to generate the appropriate package for your system and
|
||||||
|
install it via the package manager.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See Documentation and man pages
|
||||||
|
|
0
__init__.py
Normal file
0
__init__.py
Normal file
0
remail/__init__.py
Normal file
0
remail/__init__.py
Normal file
344
remail/config.py
Normal file
344
remail/config.py
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# Configuration items
|
||||||
|
|
||||||
|
from remail.mail import email_addr_valid
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class RemailException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RemailConfigException(RemailException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RemailListConfigException(RemailException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_mandatory(key, cfgdict, base):
|
||||||
|
res = cfgdict.get(key)
|
||||||
|
if not res:
|
||||||
|
txt = 'Missing config entry: %s.%s' % (base, key)
|
||||||
|
raise RemailListConfigException(txt)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_optional(key, defdict, cfgdict):
|
||||||
|
return cfgdict.get(key, defdict[key])
|
||||||
|
|
||||||
|
def set_defaults(obj, defdict, cfgdict):
|
||||||
|
if not cfgdict:
|
||||||
|
cfgdict = {}
|
||||||
|
for key, val in defdict.items():
|
||||||
|
val = cfgdict.get(key, val)
|
||||||
|
setattr(obj, key, val)
|
||||||
|
|
||||||
|
def show_attrs(obj, attrdict, indent):
|
||||||
|
for attr in attrdict:
|
||||||
|
print('%*s%-40s: %s' %(indent, '', attr, getattr(obj, attr)))
|
||||||
|
|
||||||
|
account_defaults = {
|
||||||
|
'enabled' : False,
|
||||||
|
'fingerprint' : None,
|
||||||
|
'use_smime' : False,
|
||||||
|
'gpg_plain' : False,
|
||||||
|
}
|
||||||
|
|
||||||
|
class account_config(object):
|
||||||
|
def __init__(self, cfgdict, addr, base):
|
||||||
|
base = base + '.addr'
|
||||||
|
# Do at least minimal checks for a valid email address
|
||||||
|
if not email_addr_valid(addr):
|
||||||
|
txt = 'Invalid email address: %s' % base
|
||||||
|
raise RemailListConfig_Exception(txt)
|
||||||
|
|
||||||
|
self.addr = addr
|
||||||
|
self.name = get_mandatory('name', cfgdict, base)
|
||||||
|
set_defaults(self, account_defaults, cfgdict)
|
||||||
|
|
||||||
|
# Get the optional aliases to allow sending from
|
||||||
|
# different accounts when the list is moderated
|
||||||
|
aliases = cfgdict.get('aliases')
|
||||||
|
if not aliases:
|
||||||
|
self.aliases = []
|
||||||
|
else:
|
||||||
|
self.aliases = aliases
|
||||||
|
|
||||||
|
def show(self, indent, all=True):
|
||||||
|
print('%*s%-40s: %s' %(indent, '', self.name, self.addr))
|
||||||
|
indent += 2
|
||||||
|
if all:
|
||||||
|
show_attrs(self, account_defaults, indent)
|
||||||
|
txt = ''
|
||||||
|
for alias in self.aliases:
|
||||||
|
txt += '%s ' % alias
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'aliases', txt))
|
||||||
|
else:
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'fingerprint', self.fingerprint))
|
||||||
|
|
||||||
|
class accounts_config(object):
|
||||||
|
def __init__(self, cfgdict, base):
|
||||||
|
self.accounts = {}
|
||||||
|
for addr, cfg in cfgdict.items():
|
||||||
|
account = account_config(cfg, addr, base)
|
||||||
|
self.accounts[addr] = account
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.accounts)
|
||||||
|
|
||||||
|
def has_account(self, addr):
|
||||||
|
for account in self.accounts.values():
|
||||||
|
if account.addr == addr:
|
||||||
|
return True
|
||||||
|
if addr in account.aliases:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, addr):
|
||||||
|
return self.accounts.get(addr)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self.accounts.keys()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self.accounts.values()
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
for addr, acc in self.accounts.items():
|
||||||
|
acc.show(indent)
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
addr, acc = self.accounts.popitem()
|
||||||
|
return acc
|
||||||
|
|
||||||
|
def list_account_config(cfgdict, base):
|
||||||
|
laccs = accounts_config(cfgdict, base)
|
||||||
|
if len(laccs) != 1:
|
||||||
|
txt = '% entry for %s' % base
|
||||||
|
raise RemailListConfigException(txt)
|
||||||
|
return laccs.pop()
|
||||||
|
|
||||||
|
def build_listheaders(mailaddr):
|
||||||
|
addr, domain = mailaddr.split('@')
|
||||||
|
headers = {}
|
||||||
|
headers['List-Id'] = mailaddr
|
||||||
|
headers['List-Owner'] = '<mailto:%s-owner@%s>' % (addr, domain)
|
||||||
|
headers['List-Post'] = '<mailto:%s>' % mailaddr
|
||||||
|
return headers
|
||||||
|
|
||||||
|
class destination(object):
|
||||||
|
def __init__(self, toadmin, accounts):
|
||||||
|
self.toadmin = toadmin
|
||||||
|
self.accounts = accounts
|
||||||
|
|
||||||
|
class listaddrs(object):
|
||||||
|
'''
|
||||||
|
Build an object with the valid list addresses
|
||||||
|
addr@domain, addr-owner@domain, addr-bounce@domain
|
||||||
|
'''
|
||||||
|
def __init__(self, mailaddr):
|
||||||
|
addr, domain = mailaddr.split('@')
|
||||||
|
self.post = mailaddr
|
||||||
|
self.owner = '%s-owner@%s' % (addr, domain)
|
||||||
|
self.bounce = '%s-bounce@%s' % (addr, domain)
|
||||||
|
# Dictionary to lookup an incoming address
|
||||||
|
# and to establish to which target it goes
|
||||||
|
# Entry is true if the target is admins
|
||||||
|
self.addrs = {}
|
||||||
|
self.addrs[self.post] = False
|
||||||
|
self.addrs[self.owner] = True
|
||||||
|
self.addrs[self.bounce] = True
|
||||||
|
|
||||||
|
def get_destination(self, msgto, admins, subscribers):
|
||||||
|
if not msgto in self.addrs.keys():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.addrs[msgto]:
|
||||||
|
return destination(True, admins)
|
||||||
|
return destination(False, subscribers)
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*slistaddrs:' %(indent, ''))
|
||||||
|
indent += 2
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'post', self.post))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'owner', self.owner))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'bounce', self.bounce))
|
||||||
|
|
||||||
|
class archive_config(object):
|
||||||
|
def __init__(self, aopts, listdir):
|
||||||
|
self.incoming = aopts.incoming
|
||||||
|
self.plain = aopts.plain
|
||||||
|
self.mdir = aopts.use_maildir
|
||||||
|
if self.mdir:
|
||||||
|
self.m_encr = os.path.join(listdir, 'archive_encr/')
|
||||||
|
self.m_admin = os.path.join(listdir, 'archive_admin/')
|
||||||
|
self.m_list = os.path.join(listdir, 'archive_list/')
|
||||||
|
else:
|
||||||
|
self.m_encr = os.path.join(listdir, 'archive_encr.mbox')
|
||||||
|
self.m_admin = os.path.join(listdir, 'archive_admin.mbox')
|
||||||
|
self.m_list = os.path.join(listdir, 'archive_list.mbox')
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*s%-40s: %s' % (indent, '', 'use_maildir', self.mdir))
|
||||||
|
if self.incoming:
|
||||||
|
print('%*s%-40s: %s' % (indent, '', 'incoming', self.m_encr))
|
||||||
|
if self.plain:
|
||||||
|
print('%*s%-40s: %s' % (indent, '', 'plain_admin', self.m_admin))
|
||||||
|
print('%*s%-40s: %s' % (indent, '', 'plain_list', self.m_list))
|
||||||
|
|
||||||
|
smime_defaults = {
|
||||||
|
'verify' : True,
|
||||||
|
'sign' : True,
|
||||||
|
}
|
||||||
|
|
||||||
|
class smime_config(object):
|
||||||
|
def __init__(self, listdir, cfgdict):
|
||||||
|
set_defaults(self, smime_defaults, cfgdict)
|
||||||
|
self.global_certs = '.certs'
|
||||||
|
self.ca_certs = os.path.join(self.global_certs, 'cacert.pem')
|
||||||
|
self.list_certs = os.path.normpath(os.path.join(listdir, '.certs'))
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*sS/MIME:' % (indent, ''))
|
||||||
|
indent += 2
|
||||||
|
show_attrs(self, smime_defaults, indent)
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'global_certs', self.global_certs))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'ca_certs', self.ca_certs))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'list_certs', self.list_certs))
|
||||||
|
|
||||||
|
gpg_defaults = {
|
||||||
|
'always_trust' : True,
|
||||||
|
'sign' : True,
|
||||||
|
}
|
||||||
|
|
||||||
|
class gpg_config(object):
|
||||||
|
def __init__(self, listdir, cfgdict):
|
||||||
|
set_defaults(self, gpg_defaults, cfgdict)
|
||||||
|
self.armor = True
|
||||||
|
self.home = '.gnupg'
|
||||||
|
self.keyring = os.path.join(listdir, '.gnupg', 'pubring.kbx')
|
||||||
|
self.keyring = os.path.normpath(self.keyring)
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*sGPG:' % (indent, ''))
|
||||||
|
indent += 2
|
||||||
|
show_attrs(self, gpg_defaults, indent)
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'home', self.home))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'keyring', self.keyring))
|
||||||
|
|
||||||
|
class tracking_config(object):
|
||||||
|
def __init__(self, listdir):
|
||||||
|
self.tracking_file = os.path.join(listdir, 'tracking.yaml')
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*s%-40s: %s' % (indent, '', 'tracking_file', self.tracking_file))
|
||||||
|
|
||||||
|
class template_config(object):
|
||||||
|
def __init__(self, listdir):
|
||||||
|
self.welcome = os.path.join(listdir, 'template_welcome')
|
||||||
|
self.admin = os.path.join(listdir, 'template_admin')
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*stemplates:' % (indent, ''))
|
||||||
|
indent += 2
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'welcome', self.welcome))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'admin', self.admin))
|
||||||
|
|
||||||
|
archive_defaults = {
|
||||||
|
'incoming' : True,
|
||||||
|
'plain' : True,
|
||||||
|
'use_maildir' : False,
|
||||||
|
}
|
||||||
|
|
||||||
|
class archive_options(object):
|
||||||
|
def __init__(self, cfgdict):
|
||||||
|
set_defaults(self, archive_defaults, cfgdict)
|
||||||
|
|
||||||
|
list_defaults = {
|
||||||
|
'enabled' : False,
|
||||||
|
'moderated' : False,
|
||||||
|
}
|
||||||
|
|
||||||
|
class list_config(object):
|
||||||
|
def __init__(self, name, cfgdict):
|
||||||
|
base = 'base.lists.%s' %name
|
||||||
|
self.base = base
|
||||||
|
self.name = name
|
||||||
|
set_defaults(self, list_defaults, cfgdict)
|
||||||
|
|
||||||
|
self.listdir = os.path.join('lists', name)
|
||||||
|
self.listcfg = os.path.join(self.listdir, 'list.yaml')
|
||||||
|
self.tracking = tracking_config(self.listdir)
|
||||||
|
self.templates = template_config(self.listdir)
|
||||||
|
aopts = archive_options(cfgdict.get('archive', {}))
|
||||||
|
self.archives = archive_config(aopts, self.listdir)
|
||||||
|
|
||||||
|
acc = get_mandatory('listaccount', cfgdict, base)
|
||||||
|
self.listaccount = list_account_config(acc, base + '.listaccount')
|
||||||
|
|
||||||
|
self.listaddrs = listaddrs(self.listaccount.addr)
|
||||||
|
self.listheaders = build_listheaders(self.listaccount.addr)
|
||||||
|
|
||||||
|
self.smime = smime_config(self.listdir, None)
|
||||||
|
self.gpg = gpg_config(self.listdir, None)
|
||||||
|
|
||||||
|
self.admins = accounts_config(get_mandatory('admins', cfgdict, base),
|
||||||
|
base)
|
||||||
|
self.subscribers = accounts_config({}, base)
|
||||||
|
|
||||||
|
def config_subscribers(self, cfgdict):
|
||||||
|
self.subscribers = accounts_config(cfgdict, self.base)
|
||||||
|
|
||||||
|
def show(self, indent):
|
||||||
|
print('%*s%s' %(indent, '', self.name))
|
||||||
|
indent += 2
|
||||||
|
show_attrs(self, list_defaults, indent)
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'listdir', self.listdir))
|
||||||
|
print('%*s%-40s: %s' %(indent, '', 'listcfg', self.listcfg))
|
||||||
|
self.tracking.show(indent)
|
||||||
|
print('%*sarchive:' %(indent, ''))
|
||||||
|
self.archives.show(indent + 2)
|
||||||
|
self.smime.show(indent)
|
||||||
|
self.gpg.show(indent)
|
||||||
|
print('%*slistaccount:' %(indent, ''))
|
||||||
|
self.listaccount.show(indent + 2, all=False)
|
||||||
|
self.listaddrs.show(indent)
|
||||||
|
print('%*slistheaders:' %(indent, ''))
|
||||||
|
for h, v in self.listheaders.items():
|
||||||
|
print('%*s%-40s: %s' % (indent + 2, '', h, v))
|
||||||
|
|
||||||
|
print('%*sadmins:' %(indent, ''))
|
||||||
|
self.admins.show(indent + 2)
|
||||||
|
print('%*ssubscribers:' %(indent, ''))
|
||||||
|
self.subscribers.show(indent + 2)
|
||||||
|
|
||||||
|
main_defaults = {
|
||||||
|
'enabled' : False,
|
||||||
|
'use_smtp' : False,
|
||||||
|
}
|
||||||
|
|
||||||
|
class main_config(object):
|
||||||
|
def __init__(self, cfgdict):
|
||||||
|
set_defaults(self, main_defaults, cfgdict)
|
||||||
|
self.maildir = 'maildir'
|
||||||
|
self.mailfrozen = 'maildir.frozen'
|
||||||
|
self.lockfile = 'lock'
|
||||||
|
|
||||||
|
self.smime = smime_config('.', cfgdict.get('smime'))
|
||||||
|
self.gpg = gpg_config('.', cfgdict.get('gpg'))
|
||||||
|
|
||||||
|
# Configure the lists
|
||||||
|
self.lists = []
|
||||||
|
for name, l in cfgdict.get('lists', {}).items():
|
||||||
|
self.lists.append(list_config(name, l))
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
show_attrs(self, main_defaults, 2)
|
||||||
|
print(' %-40s: %s' %('maildir', self.maildir))
|
||||||
|
self.smime.show(4)
|
||||||
|
self.gpg.show(4)
|
||||||
|
print(' lists:')
|
||||||
|
for ml in self.lists:
|
||||||
|
ml.show(4)
|
202
remail/gpg.py
Normal file
202
remail/gpg.py
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# GPG mail encryption/decryption
|
||||||
|
|
||||||
|
from remail.mail import msg_set_payload, msg_set_header
|
||||||
|
from remail.mail import msg_get_payload_as_string
|
||||||
|
from remail.mail import msg_from_string, msg_set_gpg_payload
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import string
|
||||||
|
import gnupg
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
class RemailGPGException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RemailGPGKeyException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class gpg_crypt(object):
|
||||||
|
def __init__(self, gpgcfg, account, checkkey=True):
|
||||||
|
self.config = gpgcfg
|
||||||
|
self.account = account
|
||||||
|
self.gpg = gnupg.GPG(gnupghome=self.config.home,
|
||||||
|
keyring=self.config.keyring)
|
||||||
|
self.keys = self.gpg.list_keys()
|
||||||
|
|
||||||
|
if not checkkey:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._check_key(self.keys, self.account)
|
||||||
|
# Verify that the private key is there as well
|
||||||
|
self._check_key(self.gpg.list_keys(True), self.account, private=True)
|
||||||
|
|
||||||
|
def _check_key_addr(self, key, addr):
|
||||||
|
maddr = '<%s>' %addr
|
||||||
|
maddr = maddr.lower()
|
||||||
|
for uid in key['uids']:
|
||||||
|
uid = uid.lower()
|
||||||
|
if uid.find('<') >= 0:
|
||||||
|
if uid.find(maddr) >= 0:
|
||||||
|
return
|
||||||
|
elif uid == addr:
|
||||||
|
return
|
||||||
|
|
||||||
|
txt = 'Account address/alias %s' % addr
|
||||||
|
txt += ' not in GPG key %s\nuids: ' % key['fingerprint']
|
||||||
|
for uid in key['uids']:
|
||||||
|
txt += ' %s' %uid
|
||||||
|
raise RemailGPGKeyException(txt)
|
||||||
|
|
||||||
|
def _check_key_expiry(self, key, addr):
|
||||||
|
exp = key['expires']
|
||||||
|
if not exp:
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
if now < float(exp):
|
||||||
|
return
|
||||||
|
|
||||||
|
txt = 'Account %s' % addr
|
||||||
|
txt += ' GPG key %s expired' % key['fingerprint']
|
||||||
|
raise RemailGPGKeyException(txt)
|
||||||
|
|
||||||
|
def _check_key(self, keys, account, private=False):
|
||||||
|
for key in keys:
|
||||||
|
if account.fingerprint != key['fingerprint']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Validate that it's the right one
|
||||||
|
self._check_key_addr(key, account.addr)
|
||||||
|
for alias in account.aliases:
|
||||||
|
self._check_key_addr(key, alias)
|
||||||
|
self._check_key_expiry(key, account.addr)
|
||||||
|
return
|
||||||
|
|
||||||
|
if private:
|
||||||
|
txt = 'No private key found for %s' % account.addr
|
||||||
|
else:
|
||||||
|
txt = 'No public key found for %s' % account.addr
|
||||||
|
raise RemailGPGKeyException(txt)
|
||||||
|
|
||||||
|
def check_key(self, account):
|
||||||
|
self._check_key(self.keys, account)
|
||||||
|
|
||||||
|
def do_encrypt(self, payload, fingerprints):
|
||||||
|
''' Common encryption helper'''
|
||||||
|
|
||||||
|
enc = self.gpg.encrypt(payload, fingerprints, armor=self.config.armor,
|
||||||
|
always_trust=self.config.always_trust,
|
||||||
|
sign=self.config.sign)
|
||||||
|
if enc.ok:
|
||||||
|
return str(enc)
|
||||||
|
raise RemailGPGException('Encryption fail: %s' % enc.status)
|
||||||
|
|
||||||
|
def gpg_encrypt(self, msg, account):
|
||||||
|
''' Encrypt a message for a subscriber. Depending on the
|
||||||
|
subscribers preference, inline it or use the enveloped
|
||||||
|
version.
|
||||||
|
'''
|
||||||
|
|
||||||
|
fingerprints = [str(account.fingerprint)]
|
||||||
|
payload = msg.get_payload()
|
||||||
|
|
||||||
|
# GPG inline encryption magic
|
||||||
|
|
||||||
|
# Use plain GPG if requested by accoung and possible
|
||||||
|
isplain = msg.get_content_type() == 'text/plain'
|
||||||
|
if account.gpg_plain and type(payload) == str and isplain:
|
||||||
|
encpl = self.do_encrypt(payload, fingerprints)
|
||||||
|
msg_set_payload(msg, msg_from_string(encpl))
|
||||||
|
return msg
|
||||||
|
|
||||||
|
# Extract payload for encryption
|
||||||
|
payload = msg_get_payload_as_string(msg)
|
||||||
|
encpl = self.do_encrypt(payload, fingerprints)
|
||||||
|
msg_set_gpg_payload(msg, encpl, account.addr)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def gpg_decrypt_plain(self, msg):
|
||||||
|
'''
|
||||||
|
Try to decrypt inline plain/text
|
||||||
|
|
||||||
|
If gpg decrypt returns 'no data was provided' treat the
|
||||||
|
message as unencrypted plain text.
|
||||||
|
'''
|
||||||
|
pl = msg.get_payload(decode=True)
|
||||||
|
plain = self.gpg.decrypt(pl, always_trust=self.config.always_trust)
|
||||||
|
if plain.ok:
|
||||||
|
msg_set_payload(msg, msg_from_string(str(plain)))
|
||||||
|
if plain.signature_id:
|
||||||
|
msg_set_header(msg, 'Signature-Id', plain.username)
|
||||||
|
elif plain.status != 'no data was provided':
|
||||||
|
# Check for an empty return path which is a good indicator
|
||||||
|
# for a mail server message.
|
||||||
|
rp = msg.get('Return-path')
|
||||||
|
if rp and rp != '<>':
|
||||||
|
raise RemailGPGException('Decryption failed: %s' % plain.status)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def gpg_decrypt_enveloped(self, msg):
|
||||||
|
'''
|
||||||
|
Try to decrypt an enveloped mail
|
||||||
|
'''
|
||||||
|
contents = msg.get_payload()
|
||||||
|
proto = msg.get_param('protocol')
|
||||||
|
if proto != 'application/pgp-encrypted':
|
||||||
|
raise RemailGPGException('PGP wrong protocol %s' % proto)
|
||||||
|
if len(contents) != 2:
|
||||||
|
raise RemailGPGException('PGP content length %d' % len(contents))
|
||||||
|
ct = contents[0].get_content_type()
|
||||||
|
if ct != 'application/pgp-encrypted':
|
||||||
|
raise RemailGPGException('PGP wrong app type %s' % ct)
|
||||||
|
ct = contents[1].get_content_type()
|
||||||
|
if ct != 'application/octet-stream':
|
||||||
|
raise RemailGPGException('PGP wrong app type %s' % ct)
|
||||||
|
|
||||||
|
plain = self.gpg.decrypt(contents[1].get_payload(decode=True),
|
||||||
|
always_trust=self.config.always_trust)
|
||||||
|
if not plain.ok:
|
||||||
|
raise RemailGPGException('Decryption failed: %s' % plain.status)
|
||||||
|
|
||||||
|
if plain.signature_id:
|
||||||
|
msg_set_header(msg, 'Signature-Id', plain.username)
|
||||||
|
|
||||||
|
pl = msg_from_string(str(plain))
|
||||||
|
msg_set_payload(msg, pl)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def decrypt(self, msg):
|
||||||
|
'''
|
||||||
|
Try to handle received mail with PGP. Return decoded or plain mail
|
||||||
|
'''
|
||||||
|
msgout = msg_from_string(msg.as_string())
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
|
||||||
|
if ct == 'text/plain':
|
||||||
|
return self.gpg_decrypt_plain(msgout)
|
||||||
|
|
||||||
|
elif ct == 'multipart/encrypted':
|
||||||
|
return self.gpg_decrypt_enveloped(msgout)
|
||||||
|
|
||||||
|
# There might be inline PGP with no mentioning in the content type
|
||||||
|
if not msg.is_multipart():
|
||||||
|
return msgout
|
||||||
|
|
||||||
|
payloads = msgout.get_payload()
|
||||||
|
payldecr = []
|
||||||
|
for pl in payloads:
|
||||||
|
pl = self.decrypt(pl)
|
||||||
|
payldecr.append(pl)
|
||||||
|
msgout.set_payload(payldecr)
|
||||||
|
return msgout
|
||||||
|
|
||||||
|
def encrypt(self, msg, account):
|
||||||
|
'''
|
||||||
|
Encrypt a message for a recipient
|
||||||
|
'''
|
||||||
|
return self.gpg_encrypt(msg, account)
|
||||||
|
|
462
remail/mail.py
Normal file
462
remail/mail.py
Normal file
|
@ -0,0 +1,462 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# Mail message related code
|
||||||
|
|
||||||
|
from email.utils import make_msgid, formatdate
|
||||||
|
from email.header import Header, decode_header
|
||||||
|
from email import message_from_string, message_from_bytes
|
||||||
|
from email.generator import Generator
|
||||||
|
from email.message import Message, EmailMessage
|
||||||
|
from email.policy import EmailPolicy
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
import mailbox
|
||||||
|
import hashlib
|
||||||
|
import quopri
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
def sanitize_headers(msg):
|
||||||
|
'''
|
||||||
|
Sanitize headers by keeping only the ones which are interesting
|
||||||
|
and order them as gmail is picky about that for no good reason.
|
||||||
|
'''
|
||||||
|
headers_order = [
|
||||||
|
'Return-Path',
|
||||||
|
'Date',
|
||||||
|
'From',
|
||||||
|
'To',
|
||||||
|
'Subject',
|
||||||
|
'In-Reply-To',
|
||||||
|
'References',
|
||||||
|
'User-Agent',
|
||||||
|
'MIME-Version',
|
||||||
|
'Charset',
|
||||||
|
'Message-ID',
|
||||||
|
'List-Id',
|
||||||
|
'List-Post',
|
||||||
|
'List-Owner',
|
||||||
|
'Content-Type',
|
||||||
|
'Content-Disposition',
|
||||||
|
'Content-Transfer-Encoding',
|
||||||
|
'Content-Language',
|
||||||
|
'Envelope-to',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get all headers and remove them from the message
|
||||||
|
hdrs = msg.items()
|
||||||
|
for k in msg.keys():
|
||||||
|
del msg[k]
|
||||||
|
|
||||||
|
# Add the headers back in proper order
|
||||||
|
for h in headers_order:
|
||||||
|
for k, v in hdrs:
|
||||||
|
if k.lower() == h.lower():
|
||||||
|
msg[k] = v
|
||||||
|
|
||||||
|
def send_smtp(msg, to, sender):
|
||||||
|
'''
|
||||||
|
A dumb localhost only SMTP delivery mechanism. No point in trying
|
||||||
|
to implement the world of SMTP again. KISS rules!
|
||||||
|
|
||||||
|
Any exception from the smtp transport is propagated to the caller
|
||||||
|
'''
|
||||||
|
to = msg['To']
|
||||||
|
server = smtplib.SMTP('localhost')
|
||||||
|
server.ehlo()
|
||||||
|
server.send_message(msg, sender, [to])
|
||||||
|
server.quit()
|
||||||
|
|
||||||
|
def msg_deliver(msg, account, mfrom, sender, use_smtp):
|
||||||
|
'''
|
||||||
|
Deliver the message. Replace or set the mandatory headers, sanitize
|
||||||
|
and order them properly to make gmail happy.
|
||||||
|
'''
|
||||||
|
msg_set_header(msg, 'From', encode_addr(mfrom))
|
||||||
|
msg_set_header(msg, 'To', encode_addr(account.addr))
|
||||||
|
msg_set_header(msg, 'Return-path', sender)
|
||||||
|
msg_set_header(msg, 'Envelope-to', get_raw_email_addr(account.addr))
|
||||||
|
|
||||||
|
sanitize_headers(msg)
|
||||||
|
|
||||||
|
# Set unixfrom with the current date/time
|
||||||
|
msg.set_unixfrom('From remail ' + time.ctime(time.time()))
|
||||||
|
|
||||||
|
# Send it out
|
||||||
|
mout = msg_from_string(msg.as_string().replace('\r\n', '\n'))
|
||||||
|
if use_smtp:
|
||||||
|
send_smtp(mout, account.addr, sender)
|
||||||
|
else:
|
||||||
|
print(msg.as_string())
|
||||||
|
|
||||||
|
def send_mail(msg_out, account, mfrom, sender, listheaders, use_smtp):
|
||||||
|
'''
|
||||||
|
Send mail to the account. Make sure that the message
|
||||||
|
is correct and all required headers and only necessary
|
||||||
|
headers are in the outgoing mail.
|
||||||
|
'''
|
||||||
|
# Add the list headers
|
||||||
|
for key, val in listheaders.items():
|
||||||
|
msg_out[key] = val
|
||||||
|
|
||||||
|
msg_deliver(msg_out, account, mfrom, sender, use_smtp)
|
||||||
|
|
||||||
|
# Minimal check for a valid email address
|
||||||
|
re_mail = re.compile('^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$')
|
||||||
|
|
||||||
|
def email_addr_valid(addr):
|
||||||
|
return re_mail.match(addr)
|
||||||
|
|
||||||
|
def get_raw_email_addr(addr):
|
||||||
|
'''
|
||||||
|
Return the raw mail address, name and brackets stripped off.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return addr.split('<')[1].split('>')[0].strip()
|
||||||
|
except:
|
||||||
|
return addr
|
||||||
|
|
||||||
|
re_compress_space = re.compile('\s+')
|
||||||
|
|
||||||
|
def decode_hdr(hdr):
|
||||||
|
'''
|
||||||
|
Decode a mail header with encoding
|
||||||
|
'''
|
||||||
|
elm = decode_header(hdr.strip())
|
||||||
|
res = ''
|
||||||
|
for txt, enc in elm:
|
||||||
|
# Groan ....
|
||||||
|
if enc:
|
||||||
|
res += ' ' + txt.decode(enc)
|
||||||
|
elif isinstance(txt, str):
|
||||||
|
res += ' ' + txt
|
||||||
|
else:
|
||||||
|
res += ' ' + txt.decode('ascii')
|
||||||
|
return re_compress_space.sub(' ', res).strip()
|
||||||
|
|
||||||
|
def decode_addrs(hdr):
|
||||||
|
'''
|
||||||
|
Decode mail addresses from a header and handle encondings
|
||||||
|
'''
|
||||||
|
addrs = []
|
||||||
|
if not hdr:
|
||||||
|
return addrs
|
||||||
|
parts = re_compress_space.sub(' ', hdr).split(',')
|
||||||
|
for p in parts:
|
||||||
|
addr = decode_hdr(p)
|
||||||
|
addrs.append(addr)
|
||||||
|
return addrs
|
||||||
|
|
||||||
|
def decode_from(msg):
|
||||||
|
'''
|
||||||
|
Decode the From header and return it as the topmost element
|
||||||
|
'''
|
||||||
|
addrs = decode_addrs(str(msg['From']))
|
||||||
|
return addrs.get_first()
|
||||||
|
|
||||||
|
re_noquote = re.compile('[a-zA-Z0-9_\- ]+')
|
||||||
|
|
||||||
|
def encode_hdr(txt):
|
||||||
|
try:
|
||||||
|
return txt.encode('ascii').decode()
|
||||||
|
except:
|
||||||
|
txt = txt.encode('UTF-8').decode()
|
||||||
|
|
||||||
|
def encode_addr(fulladdr):
|
||||||
|
try:
|
||||||
|
name, addr = fulladdr.split('<', 1)
|
||||||
|
name = name.strip()
|
||||||
|
except:
|
||||||
|
return fulladdr
|
||||||
|
|
||||||
|
try:
|
||||||
|
name = txt.encode('ascii').decode()
|
||||||
|
if not re_noquote.fullmatch(name):
|
||||||
|
name = '"%s"' %name.replace('"', '')
|
||||||
|
except:
|
||||||
|
name = Header(name).encode()
|
||||||
|
|
||||||
|
return name + ' <' + addr
|
||||||
|
|
||||||
|
def msg_from_string(txt):
|
||||||
|
policy = EmailPolicy(utf8=True)
|
||||||
|
return message_from_string(txt, policy=policy)
|
||||||
|
|
||||||
|
def msg_from_bytes(txt):
|
||||||
|
policy = EmailPolicy(utf8=True)
|
||||||
|
return message_from_bytes(txt, policy=policy)
|
||||||
|
|
||||||
|
def msg_force_msg_id(msg, name):
|
||||||
|
# Make sure this has a message ID
|
||||||
|
id = msg.get('Message-ID', None)
|
||||||
|
if not id:
|
||||||
|
id = make_msgid(name.split('@')[0])
|
||||||
|
msg_set_header(msg, 'Message-ID', id)
|
||||||
|
|
||||||
|
|
||||||
|
def msg_set_header(msg, hdr, txt):
|
||||||
|
'''
|
||||||
|
Set new or replace a message header
|
||||||
|
'''
|
||||||
|
for k in msg.keys():
|
||||||
|
if hdr.lower() == k.lower():
|
||||||
|
msg.replace_header(k, txt)
|
||||||
|
return
|
||||||
|
# Not found set new
|
||||||
|
msg[hdr] = txt
|
||||||
|
|
||||||
|
payload_valid_mime_headers = [
|
||||||
|
'Content-Description',
|
||||||
|
'Content-Transfer-Encoding',
|
||||||
|
'Content-Disposition',
|
||||||
|
'Content-Language',
|
||||||
|
'Content-Type',
|
||||||
|
'Charset',
|
||||||
|
'Mime-Version',
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_payload_header(hdr):
|
||||||
|
for h in payload_valid_mime_headers:
|
||||||
|
if h.lower() == hdr.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def msg_set_payload(msg, payload):
|
||||||
|
'''
|
||||||
|
Set the payload of a message.
|
||||||
|
'''
|
||||||
|
msg.clear_content()
|
||||||
|
|
||||||
|
if payload.get_content_type() == 'text/plain':
|
||||||
|
pl = payload.get_content()
|
||||||
|
msg.set_content(pl)
|
||||||
|
else:
|
||||||
|
pl = payload.get_payload()
|
||||||
|
for h, val in payload.items():
|
||||||
|
if is_payload_header(h):
|
||||||
|
msg_set_header(msg, h, val)
|
||||||
|
msg.set_payload(pl)
|
||||||
|
|
||||||
|
def msg_get_payload_as_string(msg):
|
||||||
|
'''
|
||||||
|
Get the payload with the associated and relevant headers
|
||||||
|
'''
|
||||||
|
payload = EmailMessage()
|
||||||
|
payload.set_payload(msg.get_payload())
|
||||||
|
|
||||||
|
for h in payload_valid_mime_headers:
|
||||||
|
if h in msg.keys():
|
||||||
|
payload[h] = msg[h]
|
||||||
|
elif h == 'Content-Type':
|
||||||
|
# Force Content-Type if not set
|
||||||
|
# to avoid confusing gmail
|
||||||
|
payload[h] = 'text/plain'
|
||||||
|
|
||||||
|
return payload.as_string()
|
||||||
|
|
||||||
|
def msg_set_gpg_payload(msg, encpl, bseed, addpgp=False):
|
||||||
|
# Create the message boundary
|
||||||
|
boundary = hashlib.sha1('.'.join(bseed).encode()).hexdigest() + '-' * 3
|
||||||
|
|
||||||
|
content = '--%s\n' % boundary
|
||||||
|
content += 'Content-Type: application/pgp-encrypted\n'
|
||||||
|
content += 'Content-Disposition: attachment\n\n'
|
||||||
|
content += 'Version: 1\n\n'
|
||||||
|
content += '--%s\n' % boundary
|
||||||
|
content += 'Content-Type: application/octet-stream\n'
|
||||||
|
content += 'Content-Disposition: attachment; filename="msg.asc"\n\n'
|
||||||
|
if addpgp:
|
||||||
|
content += '-----BEGIN PGP MESSAGE-----\n\n'
|
||||||
|
content += encpl + '\n'
|
||||||
|
if addpgp:
|
||||||
|
content += '-----END PGP MESSAGE-----\n\n'
|
||||||
|
|
||||||
|
msg_set_payload(msg, msg_from_string(content))
|
||||||
|
msg_set_header(msg, 'Mime-Version', '1')
|
||||||
|
msg_set_header(msg, 'Content-Type',
|
||||||
|
'multipart/encrypted; protocol="application/pgp-encrypted";boundary="%s"' % (boundary))
|
||||||
|
msg_set_header(msg, 'Content-Disposition', 'inline')
|
||||||
|
|
||||||
|
def msg_strip_signature(msg):
|
||||||
|
'''
|
||||||
|
Strip signature from msg for now. The formats are horribly different
|
||||||
|
and proper encrypted mails are signed as part of the encryption.
|
||||||
|
'''
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct != 'multipart/signed':
|
||||||
|
return msg
|
||||||
|
|
||||||
|
boundary = msg.get_boundary(None)
|
||||||
|
payload = msg.get_payload()
|
||||||
|
stripped = False
|
||||||
|
|
||||||
|
for m in payload:
|
||||||
|
if m.get_content_type() == 'application/pgp-signature':
|
||||||
|
payload.remove(m)
|
||||||
|
stripped = True
|
||||||
|
|
||||||
|
# If no signature found return unmodified msg
|
||||||
|
if not stripped:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(payload) == 1:
|
||||||
|
# If the remaining message is only a single item set it as payload
|
||||||
|
msg_set_payload(msg, payload[0])
|
||||||
|
else:
|
||||||
|
# Recreate the multipart message
|
||||||
|
content = 'Content-type: multipart/mixed; boundary="%s"\n\n' % boundary
|
||||||
|
for m in payload:
|
||||||
|
content += '--%s\n' % boundary
|
||||||
|
content += m.as_string()
|
||||||
|
content += '\n'
|
||||||
|
content += '--%s\n' % boundary
|
||||||
|
msg_set_payload(msg, msg_from_string(content))
|
||||||
|
|
||||||
|
def msg_strip_html(msg):
|
||||||
|
'''
|
||||||
|
Strip html from msg
|
||||||
|
'''
|
||||||
|
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct != 'multipart/alternative':
|
||||||
|
return
|
||||||
|
|
||||||
|
boundary = msg.get_boundary(None)
|
||||||
|
payload = msg.get_payload()
|
||||||
|
stripped = False
|
||||||
|
|
||||||
|
for m in payload:
|
||||||
|
if m.get_content_type() == 'text/html':
|
||||||
|
payload.remove(m)
|
||||||
|
stripped = True
|
||||||
|
|
||||||
|
# If no html found return
|
||||||
|
if not stripped:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(payload) == 1:
|
||||||
|
# If the remaining message is only a single item set it as payload
|
||||||
|
msg_set_payload(msg, payload[0])
|
||||||
|
else:
|
||||||
|
# Recreate the multipart message
|
||||||
|
content = 'Content-type: multipart/mixed; boundary="%s"\n\n' % boundary
|
||||||
|
for m in payload:
|
||||||
|
content += '--%s\n' % boundary
|
||||||
|
content += m.as_string()
|
||||||
|
content += '\n'
|
||||||
|
content += '--%s\n' % boundary
|
||||||
|
msg_set_payload(msg, msg_from_string(content))
|
||||||
|
|
||||||
|
def msg_sanitize_outlook(msg):
|
||||||
|
'''
|
||||||
|
Oh well ...
|
||||||
|
'''
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct != 'multipart/mixed':
|
||||||
|
return
|
||||||
|
|
||||||
|
# The bogus outlook mails consist of a text/plain and an attachment
|
||||||
|
payload = msg.get_payload()
|
||||||
|
if len(payload) != 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
if payload[0].get_content_type() != 'text/plain':
|
||||||
|
return
|
||||||
|
|
||||||
|
if payload[1].get_content_type() != 'application/octet-stream':
|
||||||
|
return
|
||||||
|
|
||||||
|
fname = payload[1].get_filename(None)
|
||||||
|
if not fname:
|
||||||
|
return
|
||||||
|
|
||||||
|
if fname != 'msg.gpg' and fname != 'msg.asc':
|
||||||
|
return
|
||||||
|
|
||||||
|
encpl = payload[1].get_payload()
|
||||||
|
msg_set_gpg_payload(msg, encpl, 'outlook', addpgp=True)
|
||||||
|
|
||||||
|
def decode_base64(msg):
|
||||||
|
#
|
||||||
|
# Decode base64 encoded text/plain sections
|
||||||
|
#
|
||||||
|
if msg.get('Content-Transfer-Encoding', '') == 'base64':
|
||||||
|
dec = base64.decodestring(msg.get_payload().encode())
|
||||||
|
msg.set_payload(dec)
|
||||||
|
del msg['Content-Transfer-Encoding']
|
||||||
|
|
||||||
|
def decode_alternative(msg):
|
||||||
|
'''
|
||||||
|
Deal with weird MUAs which put the GPG encrypted text/plain
|
||||||
|
part into a multipart/alternative mail.
|
||||||
|
'''
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct == 'multipart/alternative':
|
||||||
|
payloads = msg.get_payload()
|
||||||
|
payldec = []
|
||||||
|
for pl in payloads:
|
||||||
|
ct = pl.get_content_type()
|
||||||
|
if ct == 'text/plain':
|
||||||
|
pl = decode_base64(pl)
|
||||||
|
payldec.append(pl)
|
||||||
|
msg.set_payload(payldec)
|
||||||
|
elif ct == 'text/plain':
|
||||||
|
decode_base64(msg)
|
||||||
|
|
||||||
|
def msg_sanitize_incoming(msg):
|
||||||
|
'''
|
||||||
|
Get rid of HMTL, outlook, alternatives etc.
|
||||||
|
'''
|
||||||
|
# Strip html multipart first
|
||||||
|
msg_strip_html(msg)
|
||||||
|
|
||||||
|
# Sanitize outlook crappola
|
||||||
|
msg_sanitize_outlook(msg)
|
||||||
|
|
||||||
|
# Handle mutlipart/alternative and base64 encodings
|
||||||
|
decode_alternative(msg)
|
||||||
|
|
||||||
|
re_noreply = re.compile('^no.?reply@')
|
||||||
|
|
||||||
|
def msg_is_autoreply(msg):
|
||||||
|
'''
|
||||||
|
Check whether a message is an autoreply
|
||||||
|
'''
|
||||||
|
# RFC 3834
|
||||||
|
ar = msg.get('Auto-Submitted')
|
||||||
|
if ar and ar != 'no':
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Microsoft ...
|
||||||
|
ar = msg.get('X-Auto-Response-Suppress')
|
||||||
|
if ar in ('DR', 'AutoReply', 'All'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# precedence auto-reply
|
||||||
|
if msg.get('Precedence', '') == 'auto-reply':
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Reply-To is empty
|
||||||
|
rt = msg.get('Reply-To')
|
||||||
|
if rt:
|
||||||
|
rt = rt.strip()
|
||||||
|
if len(rt) == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if rt == '<>':
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Reply-To matches no_reply, no-reply, noreply
|
||||||
|
if re_noreply.search(rt):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Catch empty return path
|
||||||
|
rp = msg.get('Return-Path')
|
||||||
|
if not rp or rp == '<>':
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
363
remail/maillist.py
Normal file
363
remail/maillist.py
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# Mailing list related code
|
||||||
|
|
||||||
|
from remail.mail import msg_set_header, msg_force_msg_id, send_mail
|
||||||
|
from remail.mail import msg_sanitize_incoming, msg_is_autoreply
|
||||||
|
from remail.mail import get_raw_email_addr, decode_addrs
|
||||||
|
from remail.mail import msg_from_string
|
||||||
|
|
||||||
|
from remail.smime import smime_crypt, RemailSmimeException
|
||||||
|
from remail.gpg import gpg_crypt, RemailGPGException
|
||||||
|
from remail.tracking import account_tracking
|
||||||
|
from remail.config import accounts_config, gpg_config, smime_config
|
||||||
|
|
||||||
|
from email.utils import make_msgid, formatdate
|
||||||
|
from email.policy import EmailPolicy
|
||||||
|
from flufl.bounce import all_failures
|
||||||
|
|
||||||
|
import mailbox
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
|
||||||
|
class maillist(object):
|
||||||
|
'''
|
||||||
|
A class representing a mailing list
|
||||||
|
|
||||||
|
The list is configured by a preconfigured config item.
|
||||||
|
'''
|
||||||
|
def __init__(self, listcfg, logger, use_smtp):
|
||||||
|
self.logger = logger
|
||||||
|
self.config = listcfg
|
||||||
|
self.enabled = listcfg.enabled
|
||||||
|
self.use_smtp = use_smtp
|
||||||
|
|
||||||
|
self.smime = smime_crypt(self.config.smime, self.config.listaccount)
|
||||||
|
self.gpg = gpg_crypt(self.config.gpg, self.config.listaccount)
|
||||||
|
|
||||||
|
self.tracking = account_tracking(self.config.tracking, logger)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.config.name
|
||||||
|
|
||||||
|
def start_list(self):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Initilize the tracker and get the accounts which want
|
||||||
|
# a welcome mail sent to them
|
||||||
|
welcome = self.tracking.tracking_init(self.config.subscribers)
|
||||||
|
for acc in welcome:
|
||||||
|
self.send_welcome_mail(acc)
|
||||||
|
# Failure to encrypt to the receipient disables the account
|
||||||
|
# temporarily
|
||||||
|
if acc.enabled:
|
||||||
|
self.tracking.enable_account(acc)
|
||||||
|
|
||||||
|
# If exceptions happened tell the administrator
|
||||||
|
self.handle_log()
|
||||||
|
|
||||||
|
def send_plain_mail(self, msg, account):
|
||||||
|
'''
|
||||||
|
Only ever use for admin mails which contain no content!
|
||||||
|
'''
|
||||||
|
send_mail(msg, account, self.config.listaddrs.owner,
|
||||||
|
self.config.listaddrs.bounce, {}, self.use_smtp)
|
||||||
|
|
||||||
|
def encrypt(self, msg_plain, account):
|
||||||
|
'''
|
||||||
|
Encrypt plain text message for the account
|
||||||
|
'''
|
||||||
|
msg = msg_from_string(msg_plain.as_string())
|
||||||
|
if account.use_smime:
|
||||||
|
self.smime.encrypt(msg, account)
|
||||||
|
else:
|
||||||
|
self.gpg.encrypt(msg, account)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def send_encrypted_mail(self, msg_plain, account, mfrom):
|
||||||
|
try:
|
||||||
|
msg_out = self.encrypt(msg_plain, account)
|
||||||
|
send_mail(msg_out, account, mfrom, self.config.listaddrs.bounce,
|
||||||
|
self.config.listheaders, self.use_smtp)
|
||||||
|
except (RemailGPGException, RemailSmimeException) as ex:
|
||||||
|
'''
|
||||||
|
GPG and S/MIME exceptions are not fatal. If they happen
|
||||||
|
then in most cases the key/cert is not valid. Freeze the
|
||||||
|
subscribers account. The log handling will inform the
|
||||||
|
administrator about the problem.
|
||||||
|
'''
|
||||||
|
txt = 'Failed to encrypt mail to %s' % (account.addr)
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
if account in self.config.subscribers.values():
|
||||||
|
txt = 'Account frozen: %s\n' % (account.addr)
|
||||||
|
self.logger.log_warn(txt)
|
||||||
|
account.enabled = False
|
||||||
|
self.tracking.freeze_account(account)
|
||||||
|
|
||||||
|
def prepare_mail_msg(self, txt):
|
||||||
|
'''
|
||||||
|
Prepare a mail message from a string. Used for sending
|
||||||
|
template based mails to subscribers and admins.
|
||||||
|
'''
|
||||||
|
msg = msg_from_string(txt)
|
||||||
|
# Force a message id and set the date header
|
||||||
|
msg_force_msg_id(msg, self.config.listaddrs.post)
|
||||||
|
msg_set_header(msg, 'Date', formatdate())
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def send_welcome_mail(self, account):
|
||||||
|
# Read the template and replace the optional $NAME
|
||||||
|
# placeholder with the subscribers name
|
||||||
|
txt = open(self.config.templates.welcome).read()
|
||||||
|
txt = txt.replace('$NAME', account.name)
|
||||||
|
msg = self.prepare_mail_msg(txt)
|
||||||
|
self.send_encrypted_mail(msg, account, self.config.listaddrs.post)
|
||||||
|
|
||||||
|
def archive_mail(self, msg, incoming=False, admin=False):
|
||||||
|
'''
|
||||||
|
Archive mail depending on configuration.
|
||||||
|
'''
|
||||||
|
if incoming and self.config.archives.incoming:
|
||||||
|
f = self.config.archives.m_encr
|
||||||
|
elif admin and self.config.archives.plain:
|
||||||
|
f = self.config.archives.m_admin
|
||||||
|
elif not admin and self.config.archives.plain:
|
||||||
|
f = self.config.archives.m_list
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if self.config.archives.mdir:
|
||||||
|
mbox = mailbox.Maildir(f, create=True)
|
||||||
|
else:
|
||||||
|
mbox = mailbox.mbox(f, create=True)
|
||||||
|
mbox.add(msg)
|
||||||
|
mbox.close()
|
||||||
|
|
||||||
|
def decrypt_mail(self, msg):
|
||||||
|
'''
|
||||||
|
Decrypt mail after sanitizing it from HTML and outlook magic
|
||||||
|
and decoding base64 transport.
|
||||||
|
'''
|
||||||
|
msg_sanitize_incoming(msg)
|
||||||
|
|
||||||
|
msg_plain = self.smime.decrypt(msg)
|
||||||
|
if not msg_plain:
|
||||||
|
msg_plain = self.gpg.decrypt(msg)
|
||||||
|
return msg_plain
|
||||||
|
|
||||||
|
def disable_subscribers(self, addrs, msgid):
|
||||||
|
'''
|
||||||
|
Disable subscribers in @addrs. Emit a warning when an address is
|
||||||
|
newly disabled. Update state tracking.
|
||||||
|
Admins are informed via log handling
|
||||||
|
'''
|
||||||
|
for addr in addrs:
|
||||||
|
addr = addr.decode()
|
||||||
|
acc = self.config.subscribers.get(addr)
|
||||||
|
if acc:
|
||||||
|
if acc.enabled:
|
||||||
|
acc.enabled = False
|
||||||
|
txt = 'Freezing account due to permanent failure %s\n' % addr
|
||||||
|
self.logger.log_warn('txt')
|
||||||
|
self.tracking.freeze_account(acc)
|
||||||
|
else:
|
||||||
|
txt = 'Trying to freeze non existing account %s.\n' % addr
|
||||||
|
txt += ' Message-ID: %s\n' % msgid
|
||||||
|
self.logger.log_warn(txt)
|
||||||
|
|
||||||
|
def modsubject(self, msg, prefix):
|
||||||
|
'''
|
||||||
|
Add a prefix to the subject. Used for administrator mails to inform
|
||||||
|
about the nature of the information, e.g. Moderation, Bounces etc.
|
||||||
|
'''
|
||||||
|
subj = prefix + msg['Subject']
|
||||||
|
msg_set_header(msg, 'Subject', subj)
|
||||||
|
|
||||||
|
def moderate(self, msg, dest):
|
||||||
|
'''
|
||||||
|
If the list is moderated make sure that the sender of a mail
|
||||||
|
is subscribed or an administrator. This checks also aliases.
|
||||||
|
'''
|
||||||
|
if not self.config.moderated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
mfrom = get_raw_email_addr(msg.get('From'))
|
||||||
|
r = self.config.subscribers.has_account(mfrom)
|
||||||
|
r = r or self.config.admins.has_account(mfrom)
|
||||||
|
if not r:
|
||||||
|
self.modsubject(msg, '[MODERATED] ')
|
||||||
|
dest.toadmins = True
|
||||||
|
dest.accounts = self.config.admins
|
||||||
|
|
||||||
|
def check_bounces(self, msg, dest):
|
||||||
|
'''
|
||||||
|
Catch bounces and autoreply messages.
|
||||||
|
'''
|
||||||
|
temp_fail, perm_fail = all_failures(msg)
|
||||||
|
|
||||||
|
# Disable all permanent failing addresses
|
||||||
|
self.disable_subscribers(perm_fail, msg.get('Message-Id'))
|
||||||
|
|
||||||
|
# If this is a bounce, send it to the admins
|
||||||
|
if len(temp_fail) or len(perm_fail):
|
||||||
|
if len(temp_fail):
|
||||||
|
self.modsubject(msg, '[TEMPFAIL] ')
|
||||||
|
if len(perm_fail):
|
||||||
|
self.modsubject(msg, '[FROZEN] ')
|
||||||
|
dest.toadmins = True
|
||||||
|
dest.accounts = self.config.admins
|
||||||
|
|
||||||
|
if msg_is_autoreply(msg):
|
||||||
|
self.modsubject(msg, '[AUTOREPLY] ')
|
||||||
|
dest.toadmins = True
|
||||||
|
dest.accounts = self.config.admins
|
||||||
|
|
||||||
|
def mangle_from(self, msg):
|
||||||
|
'''
|
||||||
|
Build 'From' string so the original 'From' is 'visible':
|
||||||
|
From: $LISTNAME for $ORIGINAL_FROM <$LISTADDRESS>
|
||||||
|
|
||||||
|
If $ORIGINAL_FROM does not contain a name, mangle the email
|
||||||
|
address by replacing @ with _at_
|
||||||
|
'''
|
||||||
|
mfrom = msg.get('From').split('<')[0].replace('@', '_at_').strip()
|
||||||
|
return '%s for %s <%s>' % (self.config.name, mfrom,
|
||||||
|
self.config.listaddrs.post)
|
||||||
|
|
||||||
|
def do_process_mail(self, msg, dest):
|
||||||
|
# Archive the incoming mail
|
||||||
|
self.archive_mail(msg, incoming=True)
|
||||||
|
# Destination was already established. Check for bounces first
|
||||||
|
self.check_bounces(msg, dest)
|
||||||
|
# Check for moderation
|
||||||
|
self.moderate(msg, dest)
|
||||||
|
|
||||||
|
msgid = msg.get('Message-Id', '<No ID>')
|
||||||
|
msgto = msg.get('To')
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg_plain = self.decrypt_mail(msg)
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Failed to decrypt incoming %s to %s\n' %(msgid, msgto)
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.archive_mail(msg_plain, admin=dest.toadmin)
|
||||||
|
|
||||||
|
mfrom = self.mangle_from(msg)
|
||||||
|
|
||||||
|
for account in dest.accounts.values():
|
||||||
|
if not account.enabled:
|
||||||
|
continue
|
||||||
|
self.send_encrypted_mail(msg_plain, account, mfrom)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_log(self):
|
||||||
|
'''
|
||||||
|
If the logger captured warnings send them to the list admin(s)
|
||||||
|
'''
|
||||||
|
if not len(self.logger.warnings):
|
||||||
|
return
|
||||||
|
|
||||||
|
txt = open(self.config.templates.admin).read()
|
||||||
|
txt += '\n\n%s' %self.logger.warnings
|
||||||
|
msg = self.prepare_mail_msg(txt)
|
||||||
|
|
||||||
|
self.logger.log_debug('Sending warnings to admins\n')
|
||||||
|
|
||||||
|
for account in self.config.admins.values():
|
||||||
|
if not account.enabled:
|
||||||
|
continue
|
||||||
|
# Use the bounce address as from ...
|
||||||
|
self.send_plain_mail(msg, account)
|
||||||
|
self.logger.warnings = ''
|
||||||
|
|
||||||
|
def process_mail(self, msg, dest):
|
||||||
|
txt = 'Processing %s mail: %s\n' %(self.config.name, msg.get('Message-ID'))
|
||||||
|
self.logger.log_debug(txt)
|
||||||
|
res = self.do_process_mail(msg, dest)
|
||||||
|
# Send out any warning which might have happened to the admins
|
||||||
|
self.handle_log()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_destination(self, msg):
|
||||||
|
# Handle the case where someone put several addresses on To:
|
||||||
|
addrs = decode_addrs(msg['To'])
|
||||||
|
|
||||||
|
for addr in addrs:
|
||||||
|
to = get_raw_email_addr(addr)
|
||||||
|
dest = self.config.listaddrs.get_destination(to, self.config.admins,
|
||||||
|
self.config.subscribers)
|
||||||
|
if dest:
|
||||||
|
msg_set_header(msg, 'To', to)
|
||||||
|
return dest
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_keys(self):
|
||||||
|
'''
|
||||||
|
Check and validate subscriber keys
|
||||||
|
'''
|
||||||
|
for account in self.config.subscribers.values():
|
||||||
|
if not account.enabled:
|
||||||
|
continue
|
||||||
|
if not account.use_smime:
|
||||||
|
self.gpg.check_key(account)
|
||||||
|
else:
|
||||||
|
self.smime.check_cert(account)
|
||||||
|
|
||||||
|
class maillist_checker(object):
|
||||||
|
'''
|
||||||
|
Trivial wrapper around the list to check and show the subscriber
|
||||||
|
configuration.
|
||||||
|
'''
|
||||||
|
def __init__(self, configfile, logger):
|
||||||
|
self.logger = logger
|
||||||
|
try:
|
||||||
|
cfgdict = yaml.load(open(configfile))
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Failed to load list configfile %s' %configfile
|
||||||
|
logger.log_exception(txt, ex)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.accounts = accounts_config(cfgdict.get('subscribers', {}), '')
|
||||||
|
|
||||||
|
def show_config(self):
|
||||||
|
if self.accounts:
|
||||||
|
print('Subscribers:')
|
||||||
|
self.accounts.show(2)
|
||||||
|
|
||||||
|
def show_enabled(self):
|
||||||
|
if not self.accounts:
|
||||||
|
return
|
||||||
|
|
||||||
|
subs = {}
|
||||||
|
for account in self.accounts.values():
|
||||||
|
if not account.enabled:
|
||||||
|
continue
|
||||||
|
subs[account.name] = account.addr
|
||||||
|
|
||||||
|
for name in sorted(subs.keys()):
|
||||||
|
print('%-40s %s' %(name, subs[name]))
|
||||||
|
|
||||||
|
def check_keys(self):
|
||||||
|
if not self.accounts:
|
||||||
|
return
|
||||||
|
|
||||||
|
gpgcfg = gpg_config(os.getcwd(), {})
|
||||||
|
gpg = gpg_crypt(gpgcfg, None, checkkey=False)
|
||||||
|
|
||||||
|
smimecfg = smime_config(os.getcwd(), {})
|
||||||
|
smime = smime_crypt(smimecfg, None, checkkey=False)
|
||||||
|
|
||||||
|
for account in self.accounts.values():
|
||||||
|
if not account.enabled:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if not account.use_smime:
|
||||||
|
gpg.check_key(account)
|
||||||
|
else:
|
||||||
|
smime.check_cert(account)
|
||||||
|
except Exception as ex:
|
||||||
|
self.logger.log(str(ex) + '\n')
|
388
remail/remaild.py
Normal file
388
remail/remaild.py
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
|
||||||
|
from remail.config import RemailConfigException
|
||||||
|
from remail.config import main_config
|
||||||
|
from remail.maillist import maillist
|
||||||
|
|
||||||
|
from email import message_from_binary_file
|
||||||
|
from email.policy import EmailPolicy
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
import pyinotify
|
||||||
|
import mailbox
|
||||||
|
import pathlib
|
||||||
|
import signal
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
|
||||||
|
class EventHandler(pyinotify.ProcessEvent):
|
||||||
|
"""
|
||||||
|
inotify event handler which invokes the mailer function
|
||||||
|
to enqueue new mail into the mail process queue
|
||||||
|
"""
|
||||||
|
def __init__(self, mailer):
|
||||||
|
self.mailer = mailer
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_IN_CREATE(self, event):
|
||||||
|
self.mailer.queue_newmail(event.pathname)
|
||||||
|
pass
|
||||||
|
|
||||||
|
class remaild(object):
|
||||||
|
"""
|
||||||
|
The remail daemon.
|
||||||
|
"""
|
||||||
|
def __init__(self, cfgfile, logger):
|
||||||
|
self.logger = logger
|
||||||
|
self.enabled = False
|
||||||
|
|
||||||
|
self.cfgfile = cfgfile
|
||||||
|
self.yaml = YAML()
|
||||||
|
|
||||||
|
# Maildir related data
|
||||||
|
self.inotifier = None
|
||||||
|
self.newmails = []
|
||||||
|
self.failedmails = []
|
||||||
|
|
||||||
|
self.policy = EmailPolicy(utf8=True)
|
||||||
|
|
||||||
|
# Mailing lists
|
||||||
|
self.mailinglists = []
|
||||||
|
|
||||||
|
# Signals and related data
|
||||||
|
self._should_stop = False
|
||||||
|
self._should_reload = False
|
||||||
|
self.siginstall()
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
def term_handler(self, signum, frame):
|
||||||
|
'''
|
||||||
|
SIGTERM/SIGINT handler. Sets the should stop flag and stops the
|
||||||
|
inotifier which brings the inotify watch out of the wait loop.
|
||||||
|
'''
|
||||||
|
self._should_stop = True
|
||||||
|
self.stop_inotifier()
|
||||||
|
|
||||||
|
def reload_handler(self, signum, frame):
|
||||||
|
'''
|
||||||
|
SIGTERM handler. Sets the should reload flag and stops the inotifier
|
||||||
|
which brings the inotify watch out of the wait loop.
|
||||||
|
'''
|
||||||
|
self._should_reload = True
|
||||||
|
self.stop_inotifier()
|
||||||
|
|
||||||
|
def siginstall(self):
|
||||||
|
self.sigset = (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)
|
||||||
|
signal.signal(signal.SIGINT, self.term_handler)
|
||||||
|
signal.signal(signal.SIGTERM, self.term_handler)
|
||||||
|
signal.signal(signal.SIGHUP, self.reload_handler)
|
||||||
|
|
||||||
|
def sigblock(self):
|
||||||
|
'''
|
||||||
|
Blocks the relevant signals during mail processing. After each
|
||||||
|
processed mail sigpending() is checked to handle the signals
|
||||||
|
gracefully without too much delay.
|
||||||
|
'''
|
||||||
|
signal.pthread_sigmask(signal.SIG_BLOCK, self.sigset)
|
||||||
|
|
||||||
|
def sigunblock(self):
|
||||||
|
'''
|
||||||
|
Unblock the signals again
|
||||||
|
'''
|
||||||
|
signal.pthread_sigmask(signal.SIG_UNBLOCK, self.sigset)
|
||||||
|
|
||||||
|
# The remail internal queueing
|
||||||
|
def queue_newmail(self, path):
|
||||||
|
'''
|
||||||
|
new mail queue. Contains mails which have not been processed.
|
||||||
|
Scanned from the 'new' folder in the maildir after setting
|
||||||
|
up the async inotifier to make sure that existing entries are
|
||||||
|
collected. When the inotify based processing runs new entries
|
||||||
|
are added when inotify.process_events() is invoked.
|
||||||
|
|
||||||
|
Duplicate entries are prevented. Also mails in failedmails
|
||||||
|
are ignored.
|
||||||
|
'''
|
||||||
|
if path not in self.failedmails and path not in self.newmails:
|
||||||
|
self.newmails.append(path)
|
||||||
|
|
||||||
|
def scan_maildir(self):
|
||||||
|
'''
|
||||||
|
Scan all existing mails in maildir/new and maildir/cur (not
|
||||||
|
in use yet). The scanning happens after the inotifier was
|
||||||
|
installed and before the inotify events are processed. Duplicate
|
||||||
|
entries are prevented in the enqeueing function
|
||||||
|
'''
|
||||||
|
mdir = pathlib.Path(self.config.maildir) / 'cur'
|
||||||
|
for p in mdir.iterdir():
|
||||||
|
self.queue_newmail(str(p.resolve()))
|
||||||
|
mdir = pathlib.Path(self.config.maildir) / 'new'
|
||||||
|
for p in mdir.iterdir():
|
||||||
|
self.queue_newmail(str(p.resolve()))
|
||||||
|
|
||||||
|
# Inotify related functions
|
||||||
|
def install_inotifier(self):
|
||||||
|
'''
|
||||||
|
Install an async inotifier on maildir/new to avoid polling
|
||||||
|
the maildir.
|
||||||
|
'''
|
||||||
|
wm = pyinotify.WatchManager()
|
||||||
|
self.inotifier = pyinotify.AsyncNotifier(wm, EventHandler(self))
|
||||||
|
ndir = pathlib.Path(self.config.maildir) / 'new'
|
||||||
|
wm.add_watch(str(ndir.resolve()), pyinotify.IN_CREATE)
|
||||||
|
|
||||||
|
def stop_inotifier(self):
|
||||||
|
'''
|
||||||
|
Stop the inotifier. Called from signal handlers to stop waiting
|
||||||
|
and processing either for termination or reconfiguration.
|
||||||
|
'''
|
||||||
|
if self.inotifier:
|
||||||
|
self.inotifier.stop()
|
||||||
|
self.inotifier = None
|
||||||
|
|
||||||
|
def process_events(self):
|
||||||
|
try:
|
||||||
|
# Can't disable signals here as this can block
|
||||||
|
if self.inotifier.check_events():
|
||||||
|
self.inotifier.read_events();
|
||||||
|
|
||||||
|
self.inotifier.process_events()
|
||||||
|
|
||||||
|
except AttributeError as ex:
|
||||||
|
# Handle the case gracefully where the inotifier died
|
||||||
|
# There is surely a better way to do that :)
|
||||||
|
txt = '%s' %ex
|
||||||
|
if txt.find('check_events') > 0:
|
||||||
|
pass
|
||||||
|
elif txt.find('read_events') > 0:
|
||||||
|
pass
|
||||||
|
elif txt.find('process_events') > 0:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
def move_frozen(self, mailfile):
|
||||||
|
try:
|
||||||
|
fname = os.path.basename(mailfile)
|
||||||
|
fpath = os.path.join(self.config.mailfrozen, fname)
|
||||||
|
if os.path.isfile(mailfile):
|
||||||
|
os.link(mailfile, fpath)
|
||||||
|
os.unlink(mailfile)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# The actual mail processing
|
||||||
|
def process_mail(self, queue):
|
||||||
|
'''
|
||||||
|
Process the mails which are enqueued on the internal list. Invoked
|
||||||
|
with signals disabled. After each processed mail the signals are
|
||||||
|
checked.
|
||||||
|
'''
|
||||||
|
while len(queue) and not self._should_stop and not signal.sigpending():
|
||||||
|
mailfile = queue.pop()
|
||||||
|
|
||||||
|
# Try to read the mail
|
||||||
|
try:
|
||||||
|
msg = message_from_binary_file(open(mailfile, 'rb'),
|
||||||
|
policy=self.policy)
|
||||||
|
except Exception as ex:
|
||||||
|
self.move_frozen(mailfile)
|
||||||
|
txt = 'Failed to read mail file %s. Moved to frozen\n' %(mailfile)
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check whether one of the lists will take it
|
||||||
|
processed = False
|
||||||
|
for ml in self.mailinglists:
|
||||||
|
if not ml.enabled:
|
||||||
|
continue
|
||||||
|
dest = ml.get_destination(msg)
|
||||||
|
if not dest:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
processed = ml.process_mail(msg, dest)
|
||||||
|
break
|
||||||
|
except Exception as ex:
|
||||||
|
self.failedmails.append(mailfile)
|
||||||
|
txt = 'Failed to process mail file %s\n' %(mailfile)
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
break
|
||||||
|
|
||||||
|
if processed:
|
||||||
|
os.unlink(mailfile)
|
||||||
|
else:
|
||||||
|
self.move_frozen(mailfile)
|
||||||
|
txt = 'Failed to process mail file %s.' %(mailfile)
|
||||||
|
txt += ' Moved to frozen\n.'
|
||||||
|
self.logger.log_warn(txt)
|
||||||
|
|
||||||
|
def process_mails(self):
|
||||||
|
'''
|
||||||
|
Block signals for mail processing to simplify the mail processing
|
||||||
|
as some of the invoked functions are not restarting their syscalls.
|
||||||
|
Blocking the syscalls avoids dealing with those exceptions all over
|
||||||
|
the place. The mail processing checks for pending signals after each
|
||||||
|
mail so the delay for handling them is minimal.
|
||||||
|
'''
|
||||||
|
self.sigblock()
|
||||||
|
self.process_mail(self.newmails)
|
||||||
|
self.sigunblock()
|
||||||
|
|
||||||
|
# Configuration processing
|
||||||
|
def config_list(self, ml):
|
||||||
|
'''
|
||||||
|
Configure a mailing list from the list config file
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
cfgdict = self.yaml.load(open(ml.listcfg))
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Failed to load list configfile %s' %ml.listcfg
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
txt = 'Disabling list. Waiting for reconfiguration'
|
||||||
|
self.logger.log_warn(txt)
|
||||||
|
ml.enabled = False
|
||||||
|
|
||||||
|
# If the list is disabled, nothing else to do than wait
|
||||||
|
if not ml.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
subscr = cfgdict.get('subscribers', {})
|
||||||
|
ml.config_subscribers(subscr)
|
||||||
|
except RemailConfigException as ex:
|
||||||
|
txt = 'list %s disabled. Waiting for reconfiguration' % ml.name
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
self.log_warn(txt)
|
||||||
|
ml.enabled = False
|
||||||
|
|
||||||
|
def read_config(self):
|
||||||
|
'''
|
||||||
|
Read the main configuration file and analyze it.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
cfgdict = self.yaml.load(open(self.cfgfile))
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Failed to load configfile %s.' %self.cfgfile
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
txt = 'Waiting for reconfiguration'
|
||||||
|
self.logger.log_warn(txt)
|
||||||
|
cfgdict = {}
|
||||||
|
|
||||||
|
|
||||||
|
# If remail is disabled, nothing else to do than wait
|
||||||
|
self.enabled = cfgdict.get('enabled', False)
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read the configuration
|
||||||
|
try:
|
||||||
|
self.config = main_config(cfgdict)
|
||||||
|
except RemailConfigException as ex:
|
||||||
|
txt = 'remail disabled. Waiting for reconfiguration'
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
self.enabled = False
|
||||||
|
|
||||||
|
# If remail is disabled, nothing else to do than wait
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read the mailing list subscribers
|
||||||
|
for l in self.config.lists:
|
||||||
|
self.config_list(l)
|
||||||
|
|
||||||
|
# Now set up the real mailing lists
|
||||||
|
self.mailinglists = []
|
||||||
|
for l in self.config.lists:
|
||||||
|
ml = maillist(l, self.logger, self.config.use_smtp)
|
||||||
|
self.mailinglists.append(ml)
|
||||||
|
|
||||||
|
def show_config(self):
|
||||||
|
'''
|
||||||
|
Show the configuration of this remail instance including
|
||||||
|
the mailing list configurations on stdout.
|
||||||
|
'''
|
||||||
|
self.read_config()
|
||||||
|
if not self.enabled:
|
||||||
|
print('Disabled')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.config.show()
|
||||||
|
|
||||||
|
def check_keys(self):
|
||||||
|
self.read_config()
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
for ml in self.mailinglists:
|
||||||
|
ml.check_keys()
|
||||||
|
|
||||||
|
def reconfigure(self):
|
||||||
|
"""
|
||||||
|
Check for reconfiguration request.
|
||||||
|
"""
|
||||||
|
if not self._should_reload:
|
||||||
|
return
|
||||||
|
self._should_reload = False
|
||||||
|
|
||||||
|
# Prevent concurrent updates from a notification
|
||||||
|
# mechanism or a cronjob.
|
||||||
|
fd = open('lock', 'w')
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
self.read_config()
|
||||||
|
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
for ml in self.mailinglists:
|
||||||
|
try:
|
||||||
|
ml.start_list()
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Failed to start list %s' % (ml.get_name())
|
||||||
|
self.logger.log_exception(txt, ex)
|
||||||
|
ml.enabled = False
|
||||||
|
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||||
|
|
||||||
|
def should_stop(self):
|
||||||
|
return self._should_stop
|
||||||
|
|
||||||
|
def check_config(self):
|
||||||
|
# Invoke reconfiguration if requested
|
||||||
|
self.reconfigure()
|
||||||
|
|
||||||
|
# Wait if configuration is disabled
|
||||||
|
while not self.enabled and not self.should_stop():
|
||||||
|
signal.pause()
|
||||||
|
if not self.should_stop():
|
||||||
|
self.reconfigure()
|
||||||
|
|
||||||
|
return self.should_stop()
|
||||||
|
|
||||||
|
# The runner
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
# Force configuration reload
|
||||||
|
self._should_reload = True
|
||||||
|
|
||||||
|
while not self.should_stop():
|
||||||
|
|
||||||
|
# Check configuration request and eventually wait if disabled
|
||||||
|
if self.check_config():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Install inotify watch on the maildir
|
||||||
|
self.install_inotifier()
|
||||||
|
# Scan for existing mail
|
||||||
|
self.scan_maildir()
|
||||||
|
self.process_mails()
|
||||||
|
|
||||||
|
while not self._should_stop and not self._should_reload:
|
||||||
|
try:
|
||||||
|
self.process_events()
|
||||||
|
self.process_mails()
|
||||||
|
except Exception as ex:
|
||||||
|
self.stop_inotifier()
|
||||||
|
self.logger.log_exception('', ex)
|
||||||
|
self._should_stop = True
|
247
remail/smime.py
Normal file
247
remail/smime.py
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# S/MIME decrypt/encrypt functionality
|
||||||
|
|
||||||
|
from remail.mail import msg_set_payload, get_raw_email_addr
|
||||||
|
from remail.mail import msg_get_payload_as_string, msg_set_header
|
||||||
|
from remail.mail import msg_from_string, msg_from_bytes
|
||||||
|
|
||||||
|
from M2Crypto import SMIME, BIO, Rand, X509
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
class RemailSmimeException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class smime_crypt(object):
|
||||||
|
def __init__(self, smime_cfg, account, checkkey=True):
|
||||||
|
self.config = smime_cfg
|
||||||
|
self.smime = SMIME.SMIME()
|
||||||
|
self.account = account
|
||||||
|
if self.config.verify:
|
||||||
|
self.ca_verify = 0
|
||||||
|
else:
|
||||||
|
self.ca_verify = SMIME.PKCS7_NOVERIFY
|
||||||
|
|
||||||
|
if not checkkey:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Make it explode right away if the account key is missing, broken ...
|
||||||
|
try:
|
||||||
|
self.load_account_key()
|
||||||
|
except Exception as ex:
|
||||||
|
# SMIME Exceptions are undecodable
|
||||||
|
txt = 'key or crt of %s not loadable. %s' % (self.account.addr, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
||||||
|
def check_cert(self, account):
|
||||||
|
addr = account.addr
|
||||||
|
crt = os.path.join(self.config.list_certs, addr + '.crt')
|
||||||
|
try:
|
||||||
|
x509 = X509.load_cert(crt)
|
||||||
|
subj = x509.get_subject()
|
||||||
|
nbef = x509.get_not_before()
|
||||||
|
naft = x509.get_not_after()
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Account %s. ' % account.addr
|
||||||
|
txt += 'S/MIME cert %s not loadable' % crt
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
||||||
|
txt = '/emailAddress=%s' %account.addr
|
||||||
|
if str(subj) != txt:
|
||||||
|
txt = 'Account %s. ' % account.addr
|
||||||
|
txt += 'S/MIME cert %s is not matching: %s' % (crt, subj)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
||||||
|
val = nbef.get_datetime().timestamp()
|
||||||
|
now = time.time()
|
||||||
|
if now < val:
|
||||||
|
txt = 'Account %s. ' % account.addr
|
||||||
|
txt += 'S/MIME cert %s not yet valid: %s' % (crt, nbef)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
||||||
|
val = naft.get_datetime().timestamp()
|
||||||
|
now = time.time()
|
||||||
|
if now >= val:
|
||||||
|
txt = 'Account %s. ' % account.addr
|
||||||
|
txt += 'S/MIME cert %s expired: %s' % (crt, nbef)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
||||||
|
def load_account_key(self):
|
||||||
|
addr = self.account.addr
|
||||||
|
key = os.path.join(self.config.global_certs, addr + '.key')
|
||||||
|
crt = os.path.join(self.config.list_certs, addr + '.crt')
|
||||||
|
self.smime.load_key(key, crt)
|
||||||
|
|
||||||
|
def smime_is_multipart_signed(self, msg):
|
||||||
|
'''
|
||||||
|
Check whether the message is signed must be verified and decoded
|
||||||
|
'''
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct == 'multipart/signed':
|
||||||
|
proto = msg.get_param('protocol', '')
|
||||||
|
if proto == 'application/x-pkcs7-signature':
|
||||||
|
return True
|
||||||
|
if proto == 'application/pkcs7-signature':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def smime_must_verify(self, msg):
|
||||||
|
'''
|
||||||
|
Check whether the message is signed and must be verified and decoded
|
||||||
|
'''
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct == 'application/x-pkcs7-mime' or ct == 'application/pkcs7-mime':
|
||||||
|
if msg.get_param('smime-type', '') == 'signed-data':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.smime_is_multipart_signed(msg)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def smime_verify(self, msg):
|
||||||
|
'''
|
||||||
|
Verify SMIME signed message and return the payload as email.message
|
||||||
|
'''
|
||||||
|
mfrom = get_raw_email_addr(msg['From'])
|
||||||
|
|
||||||
|
crt = os.path.join(self.config.list_certs, mfrom + '.crt')
|
||||||
|
x509 = X509.load_cert(crt)
|
||||||
|
sk = X509.X509_Stack()
|
||||||
|
sk.push(x509)
|
||||||
|
self.smime.set_x509_stack(sk)
|
||||||
|
store = X509.X509_Store()
|
||||||
|
store.load_info(self.config.ca_certs)
|
||||||
|
self.smime.set_x509_store(store)
|
||||||
|
p7_bio = BIO.MemoryBuffer(msg.as_bytes())
|
||||||
|
p7, data = SMIME.smime_load_pkcs7_bio(p7_bio)
|
||||||
|
|
||||||
|
msgout = self.smime.verify(p7, data, flags=self.ca_verify)
|
||||||
|
msg_set_header(msg, 'Signature-Id', mfrom)
|
||||||
|
return msg_from_bytes(msgout)
|
||||||
|
|
||||||
|
def smime_decrypt(self, msg):
|
||||||
|
'''
|
||||||
|
Decrypt SMIME message and replace the payload of the original message
|
||||||
|
'''
|
||||||
|
self.load_account_key()
|
||||||
|
bio = BIO.MemoryBuffer(msg.as_bytes())
|
||||||
|
p7, data = SMIME.smime_load_pkcs7_bio(bio)
|
||||||
|
msg_plain = msg_from_bytes(self.smime.decrypt(p7))
|
||||||
|
|
||||||
|
# If the message is signed as well get the content
|
||||||
|
if self.smime_must_verify(msg_plain):
|
||||||
|
msg_set_payload(msg, msg_plain)
|
||||||
|
msg_plain = self.smime_verify(msg)
|
||||||
|
|
||||||
|
msg_set_payload(msg, msg_plain)
|
||||||
|
|
||||||
|
def do_decrypt(self, msg):
|
||||||
|
'''
|
||||||
|
Try to handle received mail with S/MIME. Return the decoded mail or None
|
||||||
|
'''
|
||||||
|
if self.smime_is_multipart_signed(msg):
|
||||||
|
payload = self.smime_verify(msg)
|
||||||
|
msg_set_payload(msg, payload)
|
||||||
|
|
||||||
|
ct = msg.get_content_type()
|
||||||
|
if ct == 'application/pkcs7-mime' or ct == 'application/x-pkcs7-mime':
|
||||||
|
msgout = msg_from_string(msg.as_string())
|
||||||
|
self.smime_decrypt(msgout)
|
||||||
|
return msgout
|
||||||
|
|
||||||
|
elif self.smime_must_verify(msg):
|
||||||
|
msgout = msg_from_string(msg.as_string())
|
||||||
|
payload = self.smime_verify(msgout)
|
||||||
|
msg_set_payload(msgout, payload)
|
||||||
|
return msgout
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def decrypt(self, msg):
|
||||||
|
try:
|
||||||
|
envto = msg.get('To', None)
|
||||||
|
msgid = msg.get('Message-Id', None)
|
||||||
|
return self.do_decrypt(msg)
|
||||||
|
except SMIME.PKCS7_Error as ex:
|
||||||
|
# SMIME Exceptions are undecodable
|
||||||
|
txt = 'PKCS7 error when decrypting message '
|
||||||
|
txt += '%s to %s: %s' % (msgid, envto, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
except SMIME.SMIME_Error as ex:
|
||||||
|
# SMIME Exceptions are undecodable
|
||||||
|
txt = 'SMIME error when decrypting message '
|
||||||
|
txt += '%s to %s: %s' % (msgid, envto, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Error when decrypting message '
|
||||||
|
txt += '%s to %s: %s' % (msgid, envto, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
||||||
|
def smime_encrypt(self, msg, to):
|
||||||
|
'''
|
||||||
|
Encrypt a message for a recipient
|
||||||
|
'''
|
||||||
|
# Extract payload for encryption
|
||||||
|
payload = msg_get_payload_as_string(msg).encode()
|
||||||
|
|
||||||
|
# Sign the content if a signer is set
|
||||||
|
if self.config.sign:
|
||||||
|
self.load_account_key()
|
||||||
|
|
||||||
|
sbuf = BIO.MemoryBuffer(payload)
|
||||||
|
# Sigh. sign() defaults to sha1...
|
||||||
|
p7 = self.smime.sign(sbuf, algo='sha256')
|
||||||
|
sbuf.close()
|
||||||
|
|
||||||
|
buf = BIO.MemoryBuffer()
|
||||||
|
self.smime.write(buf, p7)
|
||||||
|
else:
|
||||||
|
buf = BIO.MemoryBuffer(payload)
|
||||||
|
|
||||||
|
# Load target cert to encrypt to.
|
||||||
|
key = os.path.join(self.config.list_certs, to + '.crt')
|
||||||
|
x509 = X509.load_cert(key)
|
||||||
|
sk = X509.X509_Stack()
|
||||||
|
sk.push(x509)
|
||||||
|
self.smime.set_x509_stack(sk)
|
||||||
|
self.smime.set_cipher(SMIME.Cipher('aes_256_cbc'))
|
||||||
|
|
||||||
|
# Encrypt the buffer.
|
||||||
|
p7 = self.smime.encrypt(buf)
|
||||||
|
buf.close()
|
||||||
|
out = BIO.MemoryBuffer()
|
||||||
|
self.smime.write(out, p7)
|
||||||
|
encmsg = msg_from_bytes(out.read())
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
# Get rid of the old style x-pkcs7 type
|
||||||
|
ct = encmsg['Content-Type']
|
||||||
|
ct = ct.replace('application/x-pkcs7-mime', 'application/pkcs7-mime')
|
||||||
|
msg_set_header(encmsg, 'Content-Type', ct)
|
||||||
|
|
||||||
|
msg_set_payload(msg, encmsg)
|
||||||
|
|
||||||
|
def encrypt(self, msg, account):
|
||||||
|
'''
|
||||||
|
Encrypt a message for a recipient
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
msgid = msg.get('Message-Id', None)
|
||||||
|
self.smime_encrypt(msg, account.addr)
|
||||||
|
except SMIME.PKCS7_Error as ex:
|
||||||
|
# SMIME Exceptions are undecodable
|
||||||
|
txt = 'PKCS7 error when encrypting message '
|
||||||
|
txt += '%s to %s: %s' % (msgid, account.addr, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
except SMIME.SMIME_Error as ex:
|
||||||
|
# SMIME Exceptions are undecodable
|
||||||
|
txt = 'SMIME error when encrypting message '
|
||||||
|
txt += '%s to %s: %s' % (msgid, account.addr, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Error when encrypting message '
|
||||||
|
txt += '%s to %s: %s' % (msgid, account.addr, ex)
|
||||||
|
raise RemailSmimeException(txt)
|
||||||
|
|
123
remail/tracking.py
Normal file
123
remail/tracking.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# Account state tracking
|
||||||
|
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
class RemailTrackerException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class account_tracking(object):
|
||||||
|
'''Subscriber account tracking in a yaml file which is not part of the
|
||||||
|
configuration as the configuration is git based and updates to it
|
||||||
|
cannot be pushed back into the repository without creating a major
|
||||||
|
trainwreck.
|
||||||
|
|
||||||
|
The local states are: 'DISABLED', 'REGISTERED', 'FROZEN', 'ENABLED'
|
||||||
|
|
||||||
|
The FROZEN state is set by bounce processing and can only be removed by
|
||||||
|
two consecutive configuration updates (disable/enable) for now. Not
|
||||||
|
the most pretty mechanism, but everything else turned out to be even
|
||||||
|
more horrible.
|
||||||
|
'''
|
||||||
|
def __init__(self, trcfg, logger):
|
||||||
|
self.logger = logger
|
||||||
|
self.config = trcfg
|
||||||
|
self.yaml = YAML()
|
||||||
|
try:
|
||||||
|
self.tracked = self.yaml.load(open(self.config.tracking_file))
|
||||||
|
except:
|
||||||
|
self.tracked = {}
|
||||||
|
|
||||||
|
def tracking_init(self, subscribers):
|
||||||
|
'''
|
||||||
|
Initialize the state tracker and safe current state
|
||||||
|
after consolidating it with the configuration file
|
||||||
|
|
||||||
|
Returns a lost of accounts which require to be sent a
|
||||||
|
welcome mail.
|
||||||
|
'''
|
||||||
|
welcome = []
|
||||||
|
for acc in subscribers.values():
|
||||||
|
# Check if the account is tracked already
|
||||||
|
if acc.addr in self.tracked:
|
||||||
|
# Check the state for consistency
|
||||||
|
state = self.tracked[acc.addr]
|
||||||
|
|
||||||
|
# Last state was disabled.
|
||||||
|
if state == 'DISABLED':
|
||||||
|
# If enabled in the config file set the state to
|
||||||
|
# registered
|
||||||
|
if acc.enabled:
|
||||||
|
state = 'REGISTERED'
|
||||||
|
welcome.append(acc)
|
||||||
|
|
||||||
|
# Last state was frozen (Bounce processing...)
|
||||||
|
elif state == 'FROZEN':
|
||||||
|
# If it's disabled in the config, remove the frozen
|
||||||
|
# state. Otherwise disable the account in the in memory
|
||||||
|
# configuration until it is really disabled in the real
|
||||||
|
# configuration file. See above.
|
||||||
|
if not acc.enabled:
|
||||||
|
state = 'DISABLED'
|
||||||
|
else:
|
||||||
|
acc.enabled = False
|
||||||
|
txt = 'Account %s frozen by tracking' % acc.addr
|
||||||
|
self.logger.log_warn(txt)
|
||||||
|
|
||||||
|
# Last state was enabled or registered
|
||||||
|
elif state == 'ENABLED' or state == 'REGISTERED':
|
||||||
|
if not acc.enabled:
|
||||||
|
state = 'DISABLED'
|
||||||
|
elif state == 'REGISTERED':
|
||||||
|
welcome.append(acc)
|
||||||
|
|
||||||
|
# Ooops. Should not happen!
|
||||||
|
else:
|
||||||
|
if not acc.enabled:
|
||||||
|
state = 'DISABLED'
|
||||||
|
else:
|
||||||
|
state = 'REGISTERED'
|
||||||
|
welcome.append(acc)
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Untracked account
|
||||||
|
else:
|
||||||
|
if not acc.enabled:
|
||||||
|
state = 'DISABLED'
|
||||||
|
else:
|
||||||
|
state = 'REGISTERED'
|
||||||
|
welcome.append(acc)
|
||||||
|
|
||||||
|
# Update the tracking dict
|
||||||
|
self.tracked[acc.addr] = state
|
||||||
|
|
||||||
|
# Remove all accounts from the tracker which are not longer
|
||||||
|
# in the subscriber list
|
||||||
|
for addr in list(self.tracked):
|
||||||
|
if addr not in subscribers.keys():
|
||||||
|
del self.tracked[addr]
|
||||||
|
|
||||||
|
# Update the tracking file
|
||||||
|
self.dump_tracker()
|
||||||
|
return welcome
|
||||||
|
|
||||||
|
def dump_tracker(self):
|
||||||
|
try:
|
||||||
|
self.yaml.dump(self.tracked, open(self.config.tracking_file, 'w'))
|
||||||
|
except Exception as ex:
|
||||||
|
txt = 'Failed to dump to %s: %s' %(self.config.tracking_file, ex)
|
||||||
|
raise RemailTrackerException(txt)
|
||||||
|
|
||||||
|
def freeze_account(self, acc):
|
||||||
|
self.tracked[acc.addr] = 'FROZEN'
|
||||||
|
self.dump_tracker()
|
||||||
|
|
||||||
|
def enable_account(self, acc):
|
||||||
|
self.tracked[acc.addr] = 'ENABLED'
|
||||||
|
self.dump_tracker()
|
||||||
|
|
||||||
|
def get_state(self, acc):
|
||||||
|
return self.tracked[acc.addr]
|
111
remail/utils.py
Normal file
111
remail/utils.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
# Utilities, RemailException and assorted stuff
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import syslog
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
class RemailException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class logger(object):
|
||||||
|
'''
|
||||||
|
Logger implementation which can log on stderr or syslog
|
||||||
|
and provides a verbose mode to emit debug log messages
|
||||||
|
|
||||||
|
Warnings and exceptions are recorded in an internal buffer
|
||||||
|
which can be sent via mail to the admin(s)
|
||||||
|
'''
|
||||||
|
def __init__(self, use_syslog=False, verbose=False):
|
||||||
|
self.use_syslog = use_syslog
|
||||||
|
self.verbose = verbose
|
||||||
|
self.warnings = ''
|
||||||
|
self.exceptions = ''
|
||||||
|
self.syslog_warn = syslog.LOG_MAIL | syslog.LOG_WARNING
|
||||||
|
self.syslog_info = syslog.LOG_MAIL | syslog.LOG_INFO
|
||||||
|
self.syslog_debug = syslog.LOG_MAIL | syslog.LOG_DEBUG
|
||||||
|
|
||||||
|
def log_debug(self, txt):
|
||||||
|
'''
|
||||||
|
Debug log. Only active if verbose mode is enabled.
|
||||||
|
Content is not recorded.
|
||||||
|
'''
|
||||||
|
if self.verbose:
|
||||||
|
if self.use_syslog:
|
||||||
|
syslog.syslog(self.syslog_debug, txt)
|
||||||
|
else:
|
||||||
|
sys.stderr.write(txt)
|
||||||
|
|
||||||
|
def log(self, txt):
|
||||||
|
'''
|
||||||
|
Regular info log. Content is not recorded.
|
||||||
|
'''
|
||||||
|
if self.use_syslog:
|
||||||
|
syslog.syslog(self.syslog_info, txt)
|
||||||
|
else:
|
||||||
|
sys.stderr.write(txt)
|
||||||
|
|
||||||
|
def log_warn(self, txt):
|
||||||
|
'''
|
||||||
|
Warning log. Content is recorded.
|
||||||
|
'''
|
||||||
|
self.warnings += txt
|
||||||
|
if self.use_syslog:
|
||||||
|
syslog.syslog(self.syslog_warn, txt)
|
||||||
|
else:
|
||||||
|
sys.stderr.write(txt)
|
||||||
|
|
||||||
|
def log_exception(self, txt, ex, verbose=False):
|
||||||
|
'''
|
||||||
|
Exception log. Content is recorded. In verbose mode the
|
||||||
|
traceback is recorded as well.
|
||||||
|
'''
|
||||||
|
etxt = '%s: %s' % (type(ex).__name__, ex)
|
||||||
|
txt = 'REMAIL: %s: %s\n' % (txt, etxt)
|
||||||
|
if self.verbose or verbose:
|
||||||
|
txt += '%s\n' % (traceback.format_exc())
|
||||||
|
self.exceptions += txt
|
||||||
|
self.log_warn(txt)
|
||||||
|
|
||||||
|
def mergeconfig(key, cfgdict, defcfg, default):
|
||||||
|
'''
|
||||||
|
Return a merged configuration item.
|
||||||
|
|
||||||
|
@key: key to look up in @cfgdict and @defcfg
|
||||||
|
@cfgdict: Dictionary which can contain @key
|
||||||
|
@defcfg: If not None, it can contain an attr named @key
|
||||||
|
@default: Used if neither @cfgdict nor @defcfg provide an answer
|
||||||
|
|
||||||
|
If @defcfg is not None, then the attr named @key is queried. If
|
||||||
|
available it replaces @default.
|
||||||
|
|
||||||
|
If @key is in @cfgdict, then the value from @cnfdict is returned
|
||||||
|
otherwise the default
|
||||||
|
'''
|
||||||
|
if defcfg:
|
||||||
|
default = getattr(defcfg, key, default)
|
||||||
|
return cfgdict.get(key, default)
|
||||||
|
|
||||||
|
def makepath(base, path):
|
||||||
|
'''
|
||||||
|
Returns a path built from @base and @path.
|
||||||
|
|
||||||
|
If @path is an absolute path, it is returned unmodified
|
||||||
|
If @path is a relative path, it is appended to @base
|
||||||
|
|
||||||
|
Both @base and @path are user expanded, i.e ~/ or ~user/ are
|
||||||
|
expanded to absolute pathes
|
||||||
|
'''
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
if os.path.isabs(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
base = os.path.expanduser(base)
|
||||||
|
if not os.path.isabs(base):
|
||||||
|
base = os.path.abspath(base)
|
||||||
|
return os.path.join(base, path)
|
5
remail/version.py
Normal file
5
remail/version.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
|
||||||
|
__version__ = '0.3'
|
74
remail_chkcfg
Executable file
74
remail_chkcfg
Executable file
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
|
||||||
|
from remail.utils import logger
|
||||||
|
from remail.version import __version__
|
||||||
|
from remail.remaild import remaild
|
||||||
|
from remail.maillist import maillist_checker
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = ArgumentParser(description='remail daemon')
|
||||||
|
parser.add_argument('config', help='Config file')
|
||||||
|
parser.add_argument('--enabled', '-e', dest='enabled', action='store_true',
|
||||||
|
help='Show alist of enabled accounts. Implies -lnq')
|
||||||
|
parser.add_argument('--list', '-l', dest='islist', action='store_true',
|
||||||
|
help='List configuration check')
|
||||||
|
parser.add_argument('--nokeys', '-n', dest='nokeys', action='store_true',
|
||||||
|
help='Do not check keys')
|
||||||
|
parser.add_argument('--quiet', '-q', dest='quiet', action='store_true',
|
||||||
|
help='Quiet mode. Do not output the parsed config')
|
||||||
|
parser.add_argument('--verbose', '-v', dest='verbose', action='store_true',
|
||||||
|
help='Verbose logging')
|
||||||
|
parser.add_argument('--version', '-V', action='version',
|
||||||
|
version='%(prog)s {version}'.format(version=__version__))
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.enabled:
|
||||||
|
args.islist = True
|
||||||
|
args.quiet = True
|
||||||
|
args.nokeys = True
|
||||||
|
|
||||||
|
logger = logger(use_syslog=False, verbose=args.verbose)
|
||||||
|
|
||||||
|
# Change into the directory in which the config file resides
|
||||||
|
wdir = os.path.dirname(args.config)
|
||||||
|
if len(wdir):
|
||||||
|
os.chdir(wdir)
|
||||||
|
args.config = os.path.basename(args.config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not args.islist:
|
||||||
|
rd = remaild(args.config, logger)
|
||||||
|
else:
|
||||||
|
rd = maillist_checker(args.config, logger)
|
||||||
|
except Exception as ex:
|
||||||
|
'''
|
||||||
|
Exceptions which reach here are fatal
|
||||||
|
'''
|
||||||
|
logger.log_exception('', ex)
|
||||||
|
sys.exit(11)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not args.quiet:
|
||||||
|
rd.show_config()
|
||||||
|
if not args.nokeys:
|
||||||
|
rd.check_keys()
|
||||||
|
if args.enabled:
|
||||||
|
rd.show_enabled()
|
||||||
|
res = 0
|
||||||
|
except Exception as ex:
|
||||||
|
'''
|
||||||
|
Exceptions which reach here are fatal
|
||||||
|
'''
|
||||||
|
logger.log_exception('', ex)
|
||||||
|
res = 12
|
||||||
|
|
||||||
|
sys.exit(res)
|
||||||
|
|
57
remail_daemon
Executable file
57
remail_daemon
Executable file
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
# Copyright Thomas Gleixner <tglx@linutronix.de>
|
||||||
|
#
|
||||||
|
|
||||||
|
from remail.utils import logger
|
||||||
|
from remail.version import __version__
|
||||||
|
from remail.remaild import remaild
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def exit_daemon(logger, res):
|
||||||
|
logger.log('Stopped\n')
|
||||||
|
sys.exit(res)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = ArgumentParser(description='remail daemon')
|
||||||
|
parser.add_argument('config', help='Config file')
|
||||||
|
parser.add_argument('--syslog', '-s', dest='syslog', action='store_true',
|
||||||
|
help='Use syslog for logging. Default is stderr')
|
||||||
|
parser.add_argument('--verbose', '-v', dest='verbose', action='store_true',
|
||||||
|
help='Verbose logging')
|
||||||
|
parser.add_argument('--version', '-V', action='version',
|
||||||
|
version='%(prog)s {version}'.format(version=__version__))
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logger = logger(use_syslog=args.syslog, verbose=args.verbose)
|
||||||
|
logger.log("Started\n")
|
||||||
|
|
||||||
|
# Change into the directory in which the config file resides
|
||||||
|
wdir = os.path.dirname(args.config)
|
||||||
|
if len(wdir):
|
||||||
|
os.chdir(wdir)
|
||||||
|
args.config = os.path.basename(args.config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rd = remaild(args.config, logger)
|
||||||
|
except Exception as ex:
|
||||||
|
'''
|
||||||
|
Exceptions which reach here are fatal
|
||||||
|
'''
|
||||||
|
logger.log_exception('', ex, verbose=True)
|
||||||
|
exit_daemon(logger, 11)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = rd.run()
|
||||||
|
except Exception as ex:
|
||||||
|
'''
|
||||||
|
Exceptions which reach here are fatal
|
||||||
|
'''
|
||||||
|
logger.log_exception('', ex, verbose=True)
|
||||||
|
res = 12
|
||||||
|
|
||||||
|
exit_daemon(logger, res)
|
Loading…
Reference in a new issue