diff --git a/nullmailer/defaults/main.yml b/nullmailer/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4adfed4e01a1571d576f2d23626f3ba7ef61cc62
--- /dev/null
+++ b/nullmailer/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+
+nullmailer_enable_queued: false
diff --git a/nullmailer/files/nullmailer-queuec.py b/nullmailer/files/nullmailer-queuec.py
new file mode 100644
index 0000000000000000000000000000000000000000..72b4aa7f4089450e568693739dacc964fdb97179
--- /dev/null
+++ b/nullmailer/files/nullmailer-queuec.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+
+import socket
+import sys
+
+SERVER_SOCKET = '/run/nullmailer/queued.sock'
+
+# reference implementation from docs.python.org; will be in v3.9
+if not hasattr(socket.socket, 'send_fds'):
+    import array
+    def send_fds(sock, msg, fds):
+        ancdata = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds).tobytes())]
+        return sock.sendmsg([msg], ancdata)
+    socket.socket.send_fds = send_fds
+
+sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+try:
+    sock.connect(SERVER_SOCKET)
+except socket.error as msg:
+    print(msg)
+    sys.exit(255)
+
+ret = 255
+try:
+    sock.send_fds((1).to_bytes(4, 'big'), [sys.stdin.fileno()])
+    ret = int.from_bytes(sock.recv(1024), 'big')
+finally:
+    sock.close()
+
+sys.exit(ret)
diff --git a/nullmailer/files/nullmailer-queued.py b/nullmailer/files/nullmailer-queued.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccaa190f3a3b737dd7fb90278238259987d87d30
--- /dev/null
+++ b/nullmailer/files/nullmailer-queued.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+
+import os
+import socket
+import socketserver
+import struct
+import subprocess
+import sys
+
+SERVER_SOCKET = '/run/nullmailer/queued.sock'
+QUEUE_BINARY = '/usr/sbin/nullmailer-queue'
+
+# from: https://github.com/ganeti/ganeti/blob/4d8c16a0739c93400471023b3cf9adf73a037b8f/lib/netutils.py#L51
+_STRUCT_UCRED = "iII"  # pid, uid, gid
+_STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED)
+def get_peer_credentials(sock):
+    peercred = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, _STRUCT_UCRED_SIZE)
+    return struct.unpack(_STRUCT_UCRED, peercred)
+
+# reference implementation from docs.python.org; will be in v3.9
+if not hasattr(socket.socket, 'recv_fds'):
+    import array
+    def recv_fds(sock, msglen, maxfds):
+        fds = array.array("i")   # Array of ints
+        msg, ancdata, flags, addr = sock.recvmsg(msglen, socket.CMSG_LEN(maxfds * fds.itemsize))
+        for cmsg_level, cmsg_type, cmsg_data in ancdata:
+            if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
+                # Append data, ignoring any truncated integers at the end.
+                fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
+        return msg, list(fds)
+    socket.socket.recv_fds = recv_fds
+
+def drop_privs(uid, gid):
+    try:
+        os.setgid(gid)
+        os.setuid(uid)
+    except OSError as e:
+        print(f'could not drop privs to user {uid}, group {gid}, because {e}')
+        sys.exit(1)
+
+class QueueingHandler(socketserver.BaseRequestHandler):
+    def handle(self):
+        print(f'request incoming')
+        try:
+            fd = self.request.recv_fds(1024, 1)[1][0]
+        except IndexError:
+            print(f'did not receive fd')
+            try:
+                self.request.send((255).to_bytes(4, 'big'))
+            except:
+                pass
+            return
+        print(f'received fd')
+        (uid, gid) = get_peer_credentials(self.request)[1:]
+        print(f'by user: {uid}, {gid}')
+        try:
+            if sys.version_info.minor >= 9:
+                proc = subprocess.run([QUEUE_BINARY], stdin=fd, group=gid, user=uid)
+            else:
+                # This is not strictly safe with a threading server due to preexec_fn.
+                proc = subprocess.run([QUEUE_BINARY], stdin=fd, preexec_fn=lambda: drop_privs(uid, gid))
+        except Exception as e:
+            print(f'could not execute {QUEUE_BINARY}, because {e}')
+            try:
+                self.request.send((255).to_bytes(4, 'big'))
+            except:
+                pass
+            return
+        print(f'ran {QUEUE_BINARY}, returns {proc.returncode}')
+        self.request.send(proc.returncode.to_bytes(4, 'big'))
+
+if __name__ == '__main__':
+    try:
+        os.unlink(SERVER_SOCKET)
+    except OSError:
+        if os.path.exists(SERVER_SOCKET):
+            raise
+
+    with socketserver.ThreadingUnixStreamServer(SERVER_SOCKET, QueueingHandler) as server:
+        os.chmod(SERVER_SOCKET, 0o0777)
+        server.serve_forever()
diff --git a/nullmailer/files/nullmailer-queued.service b/nullmailer/files/nullmailer-queued.service
new file mode 100644
index 0000000000000000000000000000000000000000..dc8daa74ee02a2adb05ad72a720778e59b3cb2ec
--- /dev/null
+++ b/nullmailer/files/nullmailer-queued.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Enqueueing facility for restricted clients of nullmailer
+After=networking.target
+Before=php-fpm@.service
+
+[Service]
+ExecStart=/usr/local/sbin/nullmailer-queued
+RuntimeDirectory=nullmailer
+ReadWritePaths=/var/spool/nullmailer
+ProtectSystem=strict
+ProtectControlGroups=yes
+ProtectHome=yes
+PrivateTmp=yes
diff --git a/nullmailer/handlers/main.yml b/nullmailer/handlers/main.yml
index a96ee489e14d1c815c39f7dbddd3e87c6c895f6d..8b7744a973033e51436fc546567826781fbcad3c 100644
--- a/nullmailer/handlers/main.yml
+++ b/nullmailer/handlers/main.yml
@@ -1,5 +1,15 @@
 ---
-# file: roles/nullmailer/handlers/main.yml
 
 - name: restart nullmailer
-  service: name=nullmailer state=restarted
+  service:
+    name: nullmailer
+    state: restarted
+
+- name: restart nullmailer-queued
+  service:
+    name: nullmailer-queued
+    state: restarted
+
+- name: reload systemd service files
+  systemd:
+    daemon_reload: true
diff --git a/nullmailer/tasks/main.yml b/nullmailer/tasks/main.yml
index 78398f86d3b464dfd42b4f108cbfe34fa6db0d40..2afb71ed3572a7a660a6eeaf60de3ec329effc2c 100644
--- a/nullmailer/tasks/main.yml
+++ b/nullmailer/tasks/main.yml
@@ -95,4 +95,33 @@
   tags:
     - nullmailer
 
+- name: ensure there is a custom enqueueing service
+  copy:
+    src: nullmailer-queued.service
+    dest: /etc/systemd/system/
+  notify:
+    - reload systemd service files
+  when: nullmailer_enable_queued
+
 - meta: flush_handlers
+
+- name: ensure there is a custom enqueueing daemon
+  copy:
+    src: "nullmailer-{{ item }}.py"
+    dest: "/usr/local/sbin/nullmailer-{{ item }}"
+    owner: root
+    group: root
+    mode: '0755'
+  with_items:
+    - queuec
+    - queued
+  notify:
+    - restart nullmailer-queued
+  when: nullmailer_enable_queued
+
+- name: ensure the custom enqueueing service is enabled and running
+  service:
+    name: nullmailer-queued
+    state: started
+    enabled: true
+  when: nullmailer_enable_queued