From 25487795779c05ff8e97680550948443924b98c0 Mon Sep 17 00:00:00 2001 From: J08nY Date: Wed, 7 Jun 2017 01:27:54 +0200 Subject: Sketch out basic skeleton: - Encrypted mailing list is created with one of the "Encrypted" styles. Which creates an EncryptedMilingList object in pgpmailman's db. This object lives there for the rest of the life of the mailing list. Holds list configuration, such as action to take on unsigned messages, action to take on non- encrypted messages and so on. See pgpmailman.model.list... - Custom Incoming runner is set instead of the default Mailman one. This runner passes any messages to an ordinary list to the original incoming runner to its (configurable) queue. Any messages for an encrypted mailing list are processed before either being passed to the original incoming runner or discarded / bounced. In case they are to be accepted, they are "unwrapped" of the PGP/MIME related stuff and only the core message is passed to the original incoming runner. The signature is stripped and stored in msgdata if present, for the OutgoingRunner to attach it if configured. --- src/pgpmailman/commands/eml_key.py | 2 +- src/pgpmailman/config/__init__.py | 8 ++++++ src/pgpmailman/config/mailman.cfg | 18 +++++++++++++ src/pgpmailman/config/pgpmailman.cfg | 10 ++++++++ src/pgpmailman/database/__init__.py | 36 ++++++++++++++++++++++++++ src/pgpmailman/model/__init__.py | 0 src/pgpmailman/model/base.py | 8 ++++++ src/pgpmailman/model/list.py | 24 ++++++++++++++++++ src/pgpmailman/plugin.py | 34 ++++++++++++++++++------- src/pgpmailman/rest/lists.py | 49 ++++++++++++++++++++++++++++++++---- src/pgpmailman/rest/root.py | 11 ++++---- src/pgpmailman/rest/users.py | 1 - src/pgpmailman/runners/incoming.py | 30 +++++++++++++++++++--- src/pgpmailman/runners/outgoing.py | 17 +++++++++++-- src/pgpmailman/styles/announce.py | 11 +++++--- src/pgpmailman/styles/base.py | 23 +++++++++++++++++ src/pgpmailman/styles/discussion.py | 10 ++++++-- 17 files changed, 260 insertions(+), 32 deletions(-) create mode 100644 src/pgpmailman/config/__init__.py create mode 100644 src/pgpmailman/config/mailman.cfg create mode 100644 src/pgpmailman/config/pgpmailman.cfg create mode 100644 src/pgpmailman/model/__init__.py create mode 100644 src/pgpmailman/model/base.py create mode 100644 src/pgpmailman/model/list.py create mode 100644 src/pgpmailman/styles/base.py (limited to 'src') diff --git a/src/pgpmailman/commands/eml_key.py b/src/pgpmailman/commands/eml_key.py index ba538fa..03c0877 100644 --- a/src/pgpmailman/commands/eml_key.py +++ b/src/pgpmailman/commands/eml_key.py @@ -13,7 +13,7 @@ class KeyCommand: short_description = '' description = '' - def process(mlist, msg, msgdata, arguments, results): + def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" if len(arguments) == 0: print('No sub-command specified,' diff --git a/src/pgpmailman/config/__init__.py b/src/pgpmailman/config/__init__.py new file mode 100644 index 0000000..a6f7004 --- /dev/null +++ b/src/pgpmailman/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/pgpmailman/config/mailman.cfg b/src/pgpmailman/config/mailman.cfg new file mode 100644 index 0000000..38ff416 --- /dev/null +++ b/src/pgpmailman/config/mailman.cfg @@ -0,0 +1,18 @@ + +[plugin.pgp] +class: pgpmailman.plugin.PGPMailman +path: pgpmailman +enable: yes +configuration: python:pgpmailman.config.pgpmailman + +[runner.in] +class: pgpmailman.runners.incoming.IncomingRunner + +[runner.in_default] +class: mailman.runners.incoming.IncomingRunner + +[runner.out] +class: pgpmailman.runners.outgoing.OutgoingRunner + +[runner.out_default] +class: mailman.runners.outgoing.OutgoingRunner diff --git a/src/pgpmailman/config/pgpmailman.cfg b/src/pgpmailman/config/pgpmailman.cfg new file mode 100644 index 0000000..541e9c6 --- /dev/null +++ b/src/pgpmailman/config/pgpmailman.cfg @@ -0,0 +1,10 @@ +[db] +url = sqlite:////$DATA_DIR/pgp.db + +[keyrings] +core = $DATA_DIR/pgp_core.gpp +users = $DATA_DIR/pgp_users.gpg + +[queues] +in = in_default +out = out_default \ No newline at end of file diff --git a/src/pgpmailman/database/__init__.py b/src/pgpmailman/database/__init__.py index e69de29..01975ad 100644 --- a/src/pgpmailman/database/__init__.py +++ b/src/pgpmailman/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 pgpmailman.config import config +from pgpmailman.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/pgpmailman/model/__init__.py b/src/pgpmailman/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pgpmailman/model/base.py b/src/pgpmailman/model/base.py new file mode 100644 index 0000000..e25407f --- /dev/null +++ b/src/pgpmailman/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/pgpmailman/model/list.py b/src/pgpmailman/model/list.py new file mode 100644 index 0000000..98b0078 --- /dev/null +++ b/src/pgpmailman/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 pgpmailman.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/pgpmailman/plugin.py b/src/pgpmailman/plugin.py index 5ae51a0..820d0a9 100644 --- a/src/pgpmailman/plugin.py +++ b/src/pgpmailman/plugin.py @@ -1,21 +1,28 @@ -""" A PGP plugin for GNU Mailman.""" - +"""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 pgpmailman.rest.root import RESTRoot +from mailman.utilities.modules import expand_path from public import public from zope.interface import implementer +from pgpmailman.config import config +from pgpmailman.database import Database, transaction +from pgpmailman.model.list import EncryptedMailingList +from pgpmailman.rest.root import RESTRoot + @public @implementer(IPlugin) class PGPMailman: - - def __init__(self): - self._rest = RESTRoot() - def pre_hook(self): """See `IPlugin`.""" - pass + 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`.""" @@ -23,4 +30,13 @@ class PGPMailman: def rest_object(self): """See `IPlugin`.""" - return self._rest + 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/pgpmailman/rest/lists.py b/src/pgpmailman/rest/lists.py index 2c4a58a..fa785ee 100644 --- a/src/pgpmailman/rest/lists.py +++ b/src/pgpmailman/rest/lists.py @@ -1,15 +1,54 @@ """""" +from mailman.rest.helpers import ( + child, CollectionMixin, etag, not_found, NotFound, okay) from public import public +from pgpmailman.config import config +from pgpmailman.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: - pass +class AllEncryptedLists(_EncryptedBase): + def on_get(self, request, response): + """/lists""" + resource = self._make_collection(response) + return okay(response, etag(resource)) @public -class AnEncryptedList: +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)) - def __init__(self, list_name): - pass + @child + def key(self, context, segments): + if self._mlist is None: + return NotFound(), [] + else: + pass diff --git a/src/pgpmailman/rest/root.py b/src/pgpmailman/rest/root.py index b937fd3..68afa96 100644 --- a/src/pgpmailman/rest/root.py +++ b/src/pgpmailman/rest/root.py @@ -14,10 +14,11 @@ REST root. """ from mailman.rest.helpers import child -from pgpmailman.rest.lists import AllEncryptedLists, AnEncryptedList -from pgpmailman.rest.users import AUser, AllUsers from public import public +from pgpmailman.rest.lists import AllEncryptedLists, AnEncryptedList +from pgpmailman.rest.users import AllUsers, AUser + @public class RESTRoot: @@ -26,9 +27,8 @@ class RESTRoot: if len(segments) == 0: return AllEncryptedLists(), [] else: - list_name = segments.pop(0) - # WIP Check whether it's an encrypted list we know of here. - return AnEncryptedList(list_name), segments + list_id = segments.pop(0) + return AnEncryptedList(list_id), segments @child() def users(self, context, segments): @@ -36,5 +36,4 @@ class RESTRoot: return AllUsers(), [] else: uid = segments.pop(0) - # WIP Check whether it's an encrypted user we know of here. return AUser(uid), segments diff --git a/src/pgpmailman/rest/users.py b/src/pgpmailman/rest/users.py index bc6798d..09990f6 100644 --- a/src/pgpmailman/rest/users.py +++ b/src/pgpmailman/rest/users.py @@ -10,5 +10,4 @@ class AllUsers: @public class AUser: - pass diff --git a/src/pgpmailman/runners/incoming.py b/src/pgpmailman/runners/incoming.py index ccf90f4..69bbba9 100644 --- a/src/pgpmailman/runners/incoming.py +++ b/src/pgpmailman/runners/incoming.py @@ -1,15 +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 pgpmailman.config import config +from pgpmailman.model.list import EncryptedMailingList + @public class IncomingRunner(Runner): - def _dispose(self, mlist, msg, msgdata): + def _dispose(self, mlist: MailingList, msg: Message, msgdata: dict): """See `IRunner`.""" - pass # 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/pgpmailman/runners/outgoing.py b/src/pgpmailman/runners/outgoing.py index d7d74bc..11118a2 100644 --- a/src/pgpmailman/runners/outgoing.py +++ b/src/pgpmailman/runners/outgoing.py @@ -1,11 +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 pgpmailman.config import config +from pgpmailman.model.list import EncryptedMailingList + @public class OutgoingRunner(Runner): - def _dispose(self, mlist, msg, msgdata): + def _dispose(self, mlist: MailingList, msg: Message, msgdata: dict): """See `IRunner`.""" - pass + 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/pgpmailman/styles/announce.py b/src/pgpmailman/styles/announce.py index f83c2ea..26fc01f 100644 --- a/src/pgpmailman/styles/announce.py +++ b/src/pgpmailman/styles/announce.py @@ -3,10 +3,15 @@ from mailman.styles.default import LegacyAnnounceOnly from public import public +from pgpmailman.styles.base import EncryptedStyle + @public -class Announce(LegacyAnnounceOnly): +class AnnounceStyle(LegacyAnnounceOnly, EncryptedStyle): + name = 'encrypted-announce' + description = 'Announce only encrypted mailing list style.' + def apply(self, mailing_list): """See `IStyle`.""" - super().apply(mailing_list) - # + LegacyAnnounceOnly.apply(self, mailing_list) + EncryptedStyle.apply(self, mailing_list) diff --git a/src/pgpmailman/styles/base.py b/src/pgpmailman/styles/base.py new file mode 100644 index 0000000..633b95b --- /dev/null +++ b/src/pgpmailman/styles/base.py @@ -0,0 +1,23 @@ +"""""" + +from public import public + +from pgpmailman.config import config +from pgpmailman.database import transaction +from pgpmailman.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/pgpmailman/styles/discussion.py b/src/pgpmailman/styles/discussion.py index 55304cd..e8c516a 100644 --- a/src/pgpmailman/styles/discussion.py +++ b/src/pgpmailman/styles/discussion.py @@ -3,9 +3,15 @@ from mailman.styles.default import LegacyDefaultStyle from public import public +from pgpmailman.styles.base import EncryptedStyle + @public -class Discussion(LegacyDefaultStyle): +class DiscussionStyle(LegacyDefaultStyle, EncryptedStyle): + name = 'encrypted-default' + description = 'Ordinary discussion encrypted mailing list style.' + def apply(self, mailing_list): """See `IStyle`.""" - super().apply(mailing_list) + LegacyDefaultStyle.apply(self, mailing_list) + EncryptedStyle.apply(self, mailing_list) -- cgit v1.2.3-70-g09d2