aboutsummaryrefslogtreecommitdiff
path: root/src/mailman_pgp
diff options
context:
space:
mode:
authorJ08nY2017-06-09 17:41:24 +0200
committerJ08nY2017-06-09 17:41:24 +0200
commitb932ba4a32f208aa934bad8b4039e8c871b6715f (patch)
treec93f1277353928446dccaa4e16845fe9a83372b5 /src/mailman_pgp
parent25487795779c05ff8e97680550948443924b98c0 (diff)
downloadmailman-pgp-b932ba4a32f208aa934bad8b4039e8c871b6715f.tar.gz
mailman-pgp-b932ba4a32f208aa934bad8b4039e8c871b6715f.tar.zst
mailman-pgp-b932ba4a32f208aa934bad8b4039e8c871b6715f.zip
Diffstat (limited to 'src/mailman_pgp')
-rw-r--r--src/mailman_pgp/__init__.py0
-rw-r--r--src/mailman_pgp/archivers/__init__.py0
-rw-r--r--src/mailman_pgp/archivers/local.py14
-rw-r--r--src/mailman_pgp/archivers/remote.py14
-rw-r--r--src/mailman_pgp/commands/__init__.py0
-rw-r--r--src/mailman_pgp/commands/eml_key.py36
-rw-r--r--src/mailman_pgp/config/__init__.py8
-rw-r--r--src/mailman_pgp/config/mailman.cfg20
-rw-r--r--src/mailman_pgp/config/pgpmailman.cfg16
-rw-r--r--src/mailman_pgp/database/__init__.py36
-rw-r--r--src/mailman_pgp/model/__init__.py0
-rw-r--r--src/mailman_pgp/model/base.py8
-rw-r--r--src/mailman_pgp/model/list.py24
-rw-r--r--src/mailman_pgp/pgp/__init__.py0
-rw-r--r--src/mailman_pgp/pgp/keyrings.py3
-rw-r--r--src/mailman_pgp/plugin.py42
-rw-r--r--src/mailman_pgp/rest/__init__.py0
-rw-r--r--src/mailman_pgp/rest/lists.py54
-rw-r--r--src/mailman_pgp/rest/root.py39
-rw-r--r--src/mailman_pgp/rest/users.py13
-rw-r--r--src/mailman_pgp/runners/__init__.py0
-rw-r--r--src/mailman_pgp/runners/incoming.py39
-rw-r--r--src/mailman_pgp/runners/outgoing.py24
-rw-r--r--src/mailman_pgp/styles/__init__.py0
-rw-r--r--src/mailman_pgp/styles/announce.py17
-rw-r--r--src/mailman_pgp/styles/base.py23
-rw-r--r--src/mailman_pgp/styles/discussion.py17
27 files changed, 447 insertions, 0 deletions
diff --git a/src/mailman_pgp/__init__.py b/src/mailman_pgp/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/__init__.py
diff --git a/src/mailman_pgp/archivers/__init__.py b/src/mailman_pgp/archivers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/archivers/__init__.py
diff --git a/src/mailman_pgp/archivers/local.py b/src/mailman_pgp/archivers/local.py
new file mode 100644
index 0000000..43e08ee
--- /dev/null
+++ b/src/mailman_pgp/archivers/local.py
@@ -0,0 +1,14 @@
+"""
+Archives messages locally, encrypted (TBD how),
+similar to Mailman's prototype archiver.
+"""
+
+from mailman.interfaces.archiver import IArchiver
+from public import public
+from zope.interface import implementer
+
+
+@public
+@implementer(IArchiver)
+class LocalArchiver:
+ pass
diff --git a/src/mailman_pgp/archivers/remote.py b/src/mailman_pgp/archivers/remote.py
new file mode 100644
index 0000000..388de3a
--- /dev/null
+++ b/src/mailman_pgp/archivers/remote.py
@@ -0,0 +1,14 @@
+"""
+Archives messages by sending to django-pgpmailman,
+an extension on top of Postorius and HyperKitty.
+"""
+
+from mailman.interfaces.archiver import IArchiver
+from public import public
+from zope.interface import implementer
+
+
+@public
+@implementer(IArchiver)
+class RemoteArchiver:
+ pass
diff --git a/src/mailman_pgp/commands/__init__.py b/src/mailman_pgp/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/commands/__init__.py
diff --git a/src/mailman_pgp/commands/eml_key.py b/src/mailman_pgp/commands/eml_key.py
new file mode 100644
index 0000000..03c0877
--- /dev/null
+++ b/src/mailman_pgp/commands/eml_key.py
@@ -0,0 +1,36 @@
+"""The key email command."""
+
+from mailman.interfaces.command import ContinueProcessing, IEmailCommand
+from public import public
+from zope.interface import implementer
+
+
+@public
+@implementer(IEmailCommand)
+class KeyCommand:
+ name = 'key'
+ argument_description = '<change|revoke|sign>'
+ short_description = ''
+ description = ''
+
+ def process(self, mlist, msg, msgdata, arguments, results):
+ """See `IEmailCommand`."""
+ if len(arguments) == 0:
+ print('No sub-command specified,'
+ ' must be one of <change|revoke|sign>.', file=results)
+ return ContinueProcessing.no
+ if arguments[0] == 'change':
+ # New public key in attachment, requires to be signed with current
+ # key
+ pass
+ elif arguments[0] == 'revoke':
+ # Current key revocation certificate in attachment, restarts the
+ # subscription process, or rather only it's key setup part.
+ pass
+ elif arguments[0] == 'sign':
+ # List public key attached, signed by the users current key.
+ pass
+ else:
+ print('Wrong sub-command specified,'
+ ' must be one of <change|revoke|sign>.', file=results)
+ return ContinueProcessing.no
diff --git a/src/mailman_pgp/config/__init__.py b/src/mailman_pgp/config/__init__.py
new file mode 100644
index 0000000..a6f7004
--- /dev/null
+++ b/src/mailman_pgp/config/__init__.py
@@ -0,0 +1,8 @@
+""""""
+
+from configparser import ConfigParser
+
+from public.public import public
+
+config = ConfigParser()
+public(config=config)
diff --git a/src/mailman_pgp/config/mailman.cfg b/src/mailman_pgp/config/mailman.cfg
new file mode 100644
index 0000000..7bd45a7
--- /dev/null
+++ b/src/mailman_pgp/config/mailman.cfg
@@ -0,0 +1,20 @@
+
+# Example additions to mailman.cfg to enable PGP
+
+[plugin.pgp]
+class: mailman_pgp.plugin.PGPMailman
+path: mailman_pgp
+enable: yes
+configuration: python:mailman_pgp.config.pgpmailman
+
+[runner.in]
+class: mailman_pgp.runners.incoming.IncomingRunner
+
+[runner.in_default]
+class: mailman.runners.incoming.IncomingRunner
+
+[runner.out]
+class: mailman_pgp.runners.outgoing.OutgoingRunner
+
+[runner.out_default]
+class: mailman.runners.outgoing.OutgoingRunner
diff --git a/src/mailman_pgp/config/pgpmailman.cfg b/src/mailman_pgp/config/pgpmailman.cfg
new file mode 100644
index 0000000..45aa7b0
--- /dev/null
+++ b/src/mailman_pgp/config/pgpmailman.cfg
@@ -0,0 +1,16 @@
+# Default PGP config
+
+[db]
+# db path the PGP plugin will use to store list/user configuration (not keys!).
+url = sqlite:////$DATA_DIR/pgp.db
+
+[keyrings]
+# Keyring used to store list keypairs.
+core = $DATA_DIR/pgp_core.gpp
+
+# Keyring used to store user public keys.
+users = $DATA_DIR/pgp_users.gpg
+
+[queues]
+in = in_default
+out = out_default \ No newline at end of file
diff --git a/src/mailman_pgp/database/__init__.py b/src/mailman_pgp/database/__init__.py
new file mode 100644
index 0000000..b799942
--- /dev/null
+++ b/src/mailman_pgp/database/__init__.py
@@ -0,0 +1,36 @@
+""""""
+
+from contextlib import contextmanager
+
+from mailman.config import config as mailman_config
+from mailman.utilities.string import expand
+from public import public
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+from mailman_pgp.config import config
+from mailman_pgp.model.base import Base
+
+
+@public
+class Database:
+ def __init__(self):
+ url = config.get('db', 'url')
+ self.url = expand(url, None, mailman_config.paths)
+ self.engine = create_engine(self.url)
+ Session = sessionmaker(bind=self.engine)
+ self.session = Session()
+ Base.metadata.create_all(self.engine)
+ self.session.commit()
+
+
+@public
+@contextmanager
+def transaction():
+ try:
+ yield
+ except:
+ config.db.session.abort()
+ raise
+ else:
+ config.db.session.commit()
diff --git a/src/mailman_pgp/model/__init__.py b/src/mailman_pgp/model/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/model/__init__.py
diff --git a/src/mailman_pgp/model/base.py b/src/mailman_pgp/model/base.py
new file mode 100644
index 0000000..e25407f
--- /dev/null
+++ b/src/mailman_pgp/model/base.py
@@ -0,0 +1,8 @@
+""""""
+
+from public import public
+from sqlalchemy.ext.declarative import declarative_base
+
+
+Base = declarative_base()
+public(Base=Base)
diff --git a/src/mailman_pgp/model/list.py b/src/mailman_pgp/model/list.py
new file mode 100644
index 0000000..f781a9a
--- /dev/null
+++ b/src/mailman_pgp/model/list.py
@@ -0,0 +1,24 @@
+""""""
+
+from mailman.database.types import Enum, SAUnicode
+from mailman.interfaces.action import Action
+from public import public
+from sqlalchemy import Boolean, Column, Integer
+
+from mailman_pgp.model.base import Base
+
+
+@public
+class EncryptedMailingList(Base):
+ __tablename__ = 'encrypted_lists'
+
+ id = Column(Integer, primary_key=True)
+ list_id = Column(SAUnicode)
+ key_fingerprint = Column(SAUnicode)
+ unsigned_msg_action = Column(Enum(Action))
+ nonencrypted_msg_action = Column(Enum(Action))
+ strip_original_signature = Column(Boolean)
+ sign_outgoing = Column(Boolean)
+
+ def __init__(self, mlist):
+ self.list_id = mlist.list_id
diff --git a/src/mailman_pgp/pgp/__init__.py b/src/mailman_pgp/pgp/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/pgp/__init__.py
diff --git a/src/mailman_pgp/pgp/keyrings.py b/src/mailman_pgp/pgp/keyrings.py
new file mode 100644
index 0000000..d4fd4b4
--- /dev/null
+++ b/src/mailman_pgp/pgp/keyrings.py
@@ -0,0 +1,3 @@
+
+from public import public
+
diff --git a/src/mailman_pgp/plugin.py b/src/mailman_pgp/plugin.py
new file mode 100644
index 0000000..e6eec4f
--- /dev/null
+++ b/src/mailman_pgp/plugin.py
@@ -0,0 +1,42 @@
+"""A PGP plugin for GNU Mailman."""
+from mailman.app import events
+from mailman.config import config as mailman_config
+from mailman.interfaces.listmanager import ListDeletedEvent
+from mailman.interfaces.plugin import IPlugin
+from mailman.utilities.modules import expand_path
+from public import public
+from zope.interface import implementer
+
+from mailman_pgp.config import config
+from mailman_pgp.database import Database, transaction
+from mailman_pgp.model.list import EncryptedMailingList
+from mailman_pgp.rest.root import RESTRoot
+
+
+@public
+@implementer(IPlugin)
+class PGPMailman:
+ def pre_hook(self):
+ """See `IPlugin`."""
+ config.read(
+ expand_path(
+ dict(mailman_config.plugin_configs)[self.name].configuration))
+ config.db = Database()
+ config.name = self.name
+
+ def post_hook(self):
+ """See `IPlugin`."""
+ pass
+
+ def rest_object(self):
+ """See `IPlugin`."""
+ return RESTRoot()
+
+
+@events.subscribe(ListDeletedEvent)
+def on_delete(mlist):
+ encrypted_list = config.db.session.query(EncryptedMailingList).filter_by(
+ list_id=mlist.list_id).first()
+ if encrypted_list:
+ with transaction():
+ config.db.session.delete(encrypted_list)
diff --git a/src/mailman_pgp/rest/__init__.py b/src/mailman_pgp/rest/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/rest/__init__.py
diff --git a/src/mailman_pgp/rest/lists.py b/src/mailman_pgp/rest/lists.py
new file mode 100644
index 0000000..e0d2b0f
--- /dev/null
+++ b/src/mailman_pgp/rest/lists.py
@@ -0,0 +1,54 @@
+""""""
+
+from mailman.rest.helpers import (
+ child, CollectionMixin, etag, not_found, NotFound, okay)
+from public import public
+
+from mailman_pgp.config import config
+from mailman_pgp.model.list import EncryptedMailingList
+
+
+class _EncryptedBase(CollectionMixin):
+ def _resource_as_dict(self, emlist):
+ """See `CollectionMixin`."""
+ return dict(list_id=emlist.list_id,
+ key_fingerprint=emlist.key_fingerprint,
+ unsigned_msg_action=emlist.unsigned_msg_action,
+ nonencrypted_msg_action=emlist.nonencrypted_msg_action,
+ strip_original_signature=emlist.strip_original_signature,
+ sign_outgoing=emlist.sign_outgoing,
+ self_link=self.api.path_to(
+ '/plugins/{}/lists/{}'.format(config.name,
+ emlist.list_id)))
+
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ return config.db.session.query(EncryptedMailingList).all()
+
+
+@public
+class AllEncryptedLists(_EncryptedBase):
+ def on_get(self, request, response):
+ """/lists"""
+ resource = self._make_collection(response)
+ return okay(response, etag(resource))
+
+
+@public
+class AnEncryptedList(_EncryptedBase):
+ def __init__(self, list_id):
+ self._mlist = config.db.session.query(EncryptedMailingList).filter_by(
+ list_id=list_id).first()
+
+ def on_get(self, request, response):
+ if self._mlist is None:
+ return not_found()
+ else:
+ okay(response, self._resource_as_json(self._mlist))
+
+ @child()
+ def key(self, context, segments):
+ if self._mlist is None:
+ return NotFound(), []
+ else:
+ pass
diff --git a/src/mailman_pgp/rest/root.py b/src/mailman_pgp/rest/root.py
new file mode 100644
index 0000000..286d9ae
--- /dev/null
+++ b/src/mailman_pgp/rest/root.py
@@ -0,0 +1,39 @@
+"""
+REST root.
+
+
+/lists/ -> List all known encrypted lists.
+/lists/<list_id>/ ->
+/lists/<list_id>/key -> GET list_public_key
+/lists/<list_id>/archive/key -> GET/POST list_archive_public_key
+
+/users/ -> List all known users of encrypted lists.
+/users/<uid>/ ->
+/users/<uid>/key -> GET/POST user_public_key
+
+"""
+
+from mailman.rest.helpers import child
+from public import public
+
+from mailman_pgp.rest.lists import AllEncryptedLists, AnEncryptedList
+from mailman_pgp.rest.users import AllUsers, AUser
+
+
+@public
+class RESTRoot:
+ @child()
+ def lists(self, context, segments):
+ if len(segments) == 0:
+ return AllEncryptedLists(), []
+ else:
+ list_id = segments.pop(0)
+ return AnEncryptedList(list_id), segments
+
+ @child()
+ def users(self, context, segments):
+ if len(segments) == 0:
+ return AllUsers(), []
+ else:
+ uid = segments.pop(0)
+ return AUser(uid), segments
diff --git a/src/mailman_pgp/rest/users.py b/src/mailman_pgp/rest/users.py
new file mode 100644
index 0000000..09990f6
--- /dev/null
+++ b/src/mailman_pgp/rest/users.py
@@ -0,0 +1,13 @@
+""""""
+
+from public import public
+
+
+@public
+class AllUsers:
+ pass
+
+
+@public
+class AUser:
+ pass
diff --git a/src/mailman_pgp/runners/__init__.py b/src/mailman_pgp/runners/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/runners/__init__.py
diff --git a/src/mailman_pgp/runners/incoming.py b/src/mailman_pgp/runners/incoming.py
new file mode 100644
index 0000000..f13b971
--- /dev/null
+++ b/src/mailman_pgp/runners/incoming.py
@@ -0,0 +1,39 @@
+"""The encryption-aware incoming runner."""
+
+from mailman.config import config as mailman_config
+from mailman.core.runner import Runner
+from mailman.email.message import Message
+from mailman.model.mailinglist import MailingList
+from public import public
+
+from mailman_pgp.config import config
+from mailman_pgp.model.list import EncryptedMailingList
+
+
+@public
+class IncomingRunner(Runner):
+ def _dispose(self, mlist: MailingList, msg: Message, msgdata: dict):
+ """See `IRunner`."""
+ # Is the message for an encrypted mailing list? If not, pass to default
+ # incoming runner. If yes, go on.
+ encrypted_list = config.db.query(EncryptedMailingList).filter_by(
+ list_id=mlist.list_id).first()
+ if not encrypted_list:
+ inq = config.get('queues', 'in')
+ mailman_config.switchboards[inq].enqueue(msg, msgdata,
+ listid=mlist.list_id)
+ return False
+ # Is the message encrypted?
+ if msg.get_content_type() == 'multipart/signed' and msg.get_param(
+ 'protocol') == 'application/pgp-signature':
+ # only signed.
+ pass
+ elif msg.get_content_type() == 'multipart/encrypted' and msg.get_param(
+ 'protocol') == 'application/pgp-encrypted':
+ # definitely encrypted, might still be signed
+ pass
+ else:
+ # not encrypted or signed
+ pass
+ from email.iterators import _structure
+ _structure(msg)
diff --git a/src/mailman_pgp/runners/outgoing.py b/src/mailman_pgp/runners/outgoing.py
new file mode 100644
index 0000000..2723b17
--- /dev/null
+++ b/src/mailman_pgp/runners/outgoing.py
@@ -0,0 +1,24 @@
+"""The encryption-aware outgoing runner"""
+
+from mailman.config import config as mailman_config
+from mailman.core.runner import Runner
+from mailman.email.message import Message
+from mailman.model.mailinglist import MailingList
+from public import public
+
+from mailman_pgp.config import config
+from mailman_pgp.model.list import EncryptedMailingList
+
+
+@public
+class OutgoingRunner(Runner):
+ def _dispose(self, mlist: MailingList, msg: Message, msgdata: dict):
+ """See `IRunner`."""
+ encrypted_list = config.db.query(EncryptedMailingList).filter_by(
+ list_id=mlist.list_id).first()
+ if not encrypted_list:
+ outq = config.get('queues', 'out')
+ mailman_config.switchboards[outq].enqueue(msg,
+ msgdata,
+ listid=mlist.list_id)
+ return False
diff --git a/src/mailman_pgp/styles/__init__.py b/src/mailman_pgp/styles/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mailman_pgp/styles/__init__.py
diff --git a/src/mailman_pgp/styles/announce.py b/src/mailman_pgp/styles/announce.py
new file mode 100644
index 0000000..b4eae80
--- /dev/null
+++ b/src/mailman_pgp/styles/announce.py
@@ -0,0 +1,17 @@
+""""""
+
+from mailman.styles.default import LegacyAnnounceOnly
+from public import public
+
+from mailman_pgp.styles.base import EncryptedStyle
+
+
+@public
+class AnnounceStyle(LegacyAnnounceOnly, EncryptedStyle):
+ name = 'encrypted-announce'
+ description = 'Announce only encrypted mailing list style.'
+
+ def apply(self, mailing_list):
+ """See `IStyle`."""
+ LegacyAnnounceOnly.apply(self, mailing_list)
+ EncryptedStyle.apply(self, mailing_list)
diff --git a/src/mailman_pgp/styles/base.py b/src/mailman_pgp/styles/base.py
new file mode 100644
index 0000000..36d11c7
--- /dev/null
+++ b/src/mailman_pgp/styles/base.py
@@ -0,0 +1,23 @@
+""""""
+
+from public import public
+
+from mailman_pgp.config import config
+from mailman_pgp.database import transaction
+from mailman_pgp.model.list import EncryptedMailingList
+
+
+@public
+class EncryptedStyle:
+ def apply(self, mailing_list):
+ """Creates the encrypted mailing list instance for the list it's
+ applied to.
+ """
+ enc_list = config.db.session.query(EncryptedMailingList).filter_by(
+ list_id=mailing_list.list_id).first()
+ if enc_list:
+ return
+
+ enc_list = EncryptedMailingList(mailing_list)
+ with transaction():
+ config.db.session.add(enc_list)
diff --git a/src/mailman_pgp/styles/discussion.py b/src/mailman_pgp/styles/discussion.py
new file mode 100644
index 0000000..f9db7a7
--- /dev/null
+++ b/src/mailman_pgp/styles/discussion.py
@@ -0,0 +1,17 @@
+""""""
+
+from mailman.styles.default import LegacyDefaultStyle
+from public import public
+
+from mailman_pgp.styles.base import EncryptedStyle
+
+
+@public
+class DiscussionStyle(LegacyDefaultStyle, EncryptedStyle):
+ name = 'encrypted-default'
+ description = 'Ordinary discussion encrypted mailing list style.'
+
+ def apply(self, mailing_list):
+ """See `IStyle`."""
+ LegacyDefaultStyle.apply(self, mailing_list)
+ EncryptedStyle.apply(self, mailing_list)