A complete Raspberry Pi, mailcow, No-IP, WireGuard, and RabbitMQ build. Mail content is data and is never executed.

THE COW, THE RABBIT & THE DEAD LETTER OFFICE

A complete Raspberry Pi, mailcow, No-IP, WireGuard, and RabbitMQ build

INIT.WTF FIELD MANUAL 02

By Michael A. Cohee // INIT.WTF

Lab-ready draft 0.1 — 2026-08-11

We already had email. We already had queues. So naturally, we built another postal system between them.

What are we making—and why?

We are building a small, self-hosted email domain on a Raspberry Pi. Human beings send ordinary email into it. A bridge reads a dedicated mailbox, converts each message into a signed JSON envelope, and publishes that envelope into RabbitMQ. Each experimental node gets a durable queue: a packet mailbox.

RabbitMQ is not replacing SMTP, IMAP, or the internet. It is the programmable sorting room behind the public post office.

That boundary gives the project a reason to exist:

  • SMTP accepts mail from the outside world.

  • Mailcow authenticates, filters, signs, stores, and presents it normally.

  • IMAP remains the source of truth for the original message.

  • RabbitMQ turns accepted mail into observable internal events.

  • Topic routing sends envelopes to named nodes.

  • Durable queues hold mail while nodes are offline.

  • Manual acknowledgements expose successful processing.

  • Retries and dead-lettering make failure visible instead of mysterious.

The experiment teaches two different forms of store-and-forward messaging by forcing them to cooperate. It is not the cheapest way to get email. It is an excellent way to understand routing, identity, delivery guarantees, backpressure, protocol translation, and trust boundaries.

The finished ritual

  • Send email to red@packet.init.wtf.

  • Mailcow receives and stores the original message.

  • The bridge publishes a compact, HMAC-signed envelope.

  • RabbitMQ routes it to node.red.inbox.

  • Disconnect the red node. Its queue waits.

  • Reconnect it. The node consumes and acknowledges the envelope.

  • Send malformed or oversized mail. It lands in packetmail.dead.

That last queue is our Dead Letter Office.

What No-IP does—and does not do

Most residential public IP addresses can change. No-IP’s Dynamic Update Client watches the public IP and updates a hostname such as initmail.ddns.net. Instead of remembering today’s number, a client follows the stable name.

No-IP solves:

“What public address does my home connection have right now?”

It does not automatically solve:

  • carrier-grade NAT

  • a blocked TCP port 25

  • reverse DNS/PTR controlled by the IP owner

  • poor reputation of residential address space

  • outbound-mail filtering

  • power failures

  • an ISP prohibition on servers

Dynamic DNS is naming, not mail reputation. A DDNS hostname can reach a home service; it does not transform a residential circuit into a trustworthy internet mail exchange.

No-IP’s free hostnames currently require confirmation every 30 days. That is acceptable for a supervised lab and a bad unattended production dependency. Use a paid hostname, a custom-domain dynamic record, or a calendar-backed operational routine before relying on it.

Choose the network mode before buying anything

Mode A — Direct residential mail

Use direct mode only when all of these are true:

  • the router has a real public IPv4 or usable global IPv6 address

  • the connection is not behind CGNAT

  • the ISP permits inbound and outbound TCP 25

  • the ISP permits servers under the service agreement

  • the ISP will set a matching PTR record

  • the address is stable enough for mail reputation

  • you accept that residential deliverability may still be poor

No-IP updates the public hostname when the address changes. DNS for the experimental mail domain points directly to that hostname or to a custom A/AAAA record kept current by No-IP.

This is the fewest boxes and the most fragile public-email design.

Mode B — Static edge, home mailbox (recommended)

Use a small VPS with a static public IP and configurable PTR as the internet-facing SMTP edge. A WireGuard tunnel connects that VPS to Mailcow on the Pi. The VPS accepts only known recipients for the lab domain, forwards inbound mail through WireGuard, and accepts outbound mail only from the authenticated tunnel peer.

No-IP then serves a narrower, honest job: a stable name for optional direct home access, status checks, or the Mailcow web/IMAP endpoint. The mail domain’s MX remains on the static VPS.

flowchart TD
    I["Internet SMTP"] --> V["Static VPS: MX + PTR"]
    V -->|"WireGuard 10.77.0.0/24"| P["Home Pi: mailcow"]
    P --> B["Mail ↔ AMQP bridge"]
    B --> R["RabbitMQ packet mailboxes"]

The direct mode is documented because it is educational. The hybrid mode is recommended because it separates the public mail identity from a changing home circuit.

Bill of materials

Home node

  • Raspberry Pi 5 with 16 GB RAM recommended for the combined stack

  • Raspberry Pi 5 with 8 GB RAM acceptable if RabbitMQ moves to another node or Mailcow’s ClamAV and full-text search are disabled

  • official-quality power supply

  • active cooling

  • 256 GB or larger USB 3 SSD; do not make a microSD card the primary mail store

  • wired Ethernet

  • small UPS strongly recommended

  • router access for DHCP reservation and any deliberate port forwarding

Mailcow’s current documented floor is 6 GiB RAM plus 1 GiB swap and 20 GiB disk before mail storage. ARM64 is supported. The project is a full groupware stack—not merely Postfix—and memory use reflects that.

Internet identity

  • a domain you control; this manual isolates the experiment under packet.init.wtf

  • a No-IP hostname such as initmail.ddns.net

  • access to the authoritative DNS zone for init.wtf

  • for recommended Mode B: a small VPS with static IPv4, PTR control, and TCP 25 permitted

Workstation

  • SSH client

  • text editor

  • password manager

  • a second email account outside the lab domain for testing

Address and name plan

All addresses below are documentation examples. Replace them before deployment.

Object Example
Mail domain packet.init.wtf
Public SMTP edge mx1.packet.init.wtf
Mailcow client name home.packet.init.wtf
No-IP hostname initmail.ddns.net
Pi LAN address 192.168.50.20
VPS public address 203.0.113.25
VPS WireGuard address 10.77.0.1
Pi WireGuard address 10.77.0.2
RabbitMQ vhost /packetmail
RabbitMQ management 127.0.0.1:15672
Bridge mailbox packetbridge@packet.init.wtf
Node mailboxes red@packet.init.wtf,green@packet.init.wtf

203.0.113.25 is reserved example space. It will never be your VPS address.

Phase 0 — The go/no-go survey

Detect CGNAT

Look at the router’s WAN address and compare it with the public IPv4 shown by a reputable IP-check service or the No-IP dashboard.

If the router’s WAN address is in any of these ranges, it is not a directly routable public IPv4 address:

  • 10.0.0.0/8

  • 172.16.0.0/12

  • 192.168.0.0/16

  • 100.64.0.0/10

If the router and internet disagree, assume CGNAT until the ISP confirms otherwise. Port forwarding will not fix upstream NAT.

Test outbound SMTP

From an existing Linux machine on the home connection:

nc -vz gmail-smtp-in.l.google.com 25

A timeout or policy block suggests outbound TCP 25 is filtered. A connection proves only outbound reachability; inbound port 25 still requires a separate test from outside the home network after a deliberate forward exists.

Ask the PTR question

The owner of the public IP range controls reverse DNS. Ask the ISP whether it will set PTR for the address. Check an existing PTR with:

dig -x YOUR_PUBLIC_IP +short

If the ISP cannot set PTR to the SMTP hostname, choose Mode B. Do not try to conceal the mismatch with another CNAME.

Protect the real domain

Do not move existing personal or business mail to the experiment. A subdomain has its own MX records, SPF, DKIM, DMARC, and blast radius. Breaking packet.init.wtf should not break ordinary init.wtf mail.

Phase 1 — Build the Pi correctly

Use Raspberry Pi Imager to write Ubuntu Server 24.04 LTS ARM64 to the SSD. In the customization screen:

  • hostname: packet-pi

  • create a non-default administrator account

  • install an SSH public key

  • disable password SSH if you can reliably use the key

  • set America/New_York

  • do not configure Wi-Fi when Ethernet is available

Boot from the SSD, reserve 192.168.50.20 for the Pi in the router, and connect:

ssh youradmin@192.168.50.20

Update and install the operating-system tools:

sudo apt update
sudo apt full-upgrade
sudo apt install -y git openssl curl gawk coreutils grep jq dnsutils netcat-openbsd wireguard
sudo reboot

Verify the machine before adding state:

hostnamectl
timedatectl
hostname -I
ip route
lsblk
df -h
free -h

Mailcow requires swap even on a reasonably sized private installation. If the image has none, create 2 GiB:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

Never publish output containing real public addresses, interface identifiers, or SSH material without redaction.

Phase 2 — Install Docker from its official repository

Remove conflicting distribution packages if present:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

Add Docker’s Ubuntu repository:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Create /etc/apt/sources.list.d/docker.sources:

Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: noble
Components: stable
Architectures: arm64
Signed-By: /etc/apt/keyrings/docker.asc

Install and verify:

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
sudo docker version
sudo docker compose version

Docker-published ports can bypass ordinary UFW expectations. Mailcow specifically warns administrators to place container restrictions in the DOCKER-USER chain or an upstream firewall. For the home node, our first boundary is simple: do not create router port forwards until the corresponding phase explicitly requires one.

Phase 3 — Give the house a stable No-IP name

Create a No-IP hostname such as initmail.ddns.net. Generate a DDNS Key rather than reusing the main account password.

Install No-IP’s official DUC container:

sudo install -d -m 0700 /opt/noip-duc
sudo nano /opt/noip-duc/noip-duc.env

Enter:

NOIP_USERNAME=YOUR_DDNS_KEY_USERNAME
NOIP_PASSWORD=YOUR_DDNS_KEY_PASSWORD
NOIP_HOSTNAMES=initmail.ddns.net

Protect it and start the updater:

sudo chmod 600 /opt/noip-duc/noip-duc.env
sudo docker pull ghcr.io/noipcom/noip-duc:latest
sudo docker run -d \
  --name noip-duc \
  --restart unless-stopped \
  --env-file /opt/noip-duc/noip-duc.env \
  ghcr.io/noipcom/noip-duc:latest

Verify without displaying credentials:

sudo docker logs --tail 30 noip-duc
getent ahostsv4 initmail.ddns.net

If the hostname uses No-IP’s free tier, confirm it on the required 30-day cycle. IP updates by the DUC do not count as manual hostname confirmation.

Connect the custom name

For optional direct web/IMAP access, create this record in the init.wtf DNS zone:

home.packet.init.wtf.  CNAME  initmail.ddns.net.

That CNAME is for clients and certificate validation. It is not the recommended MX target in Mode B.

Phase 4 — Install Mailcow

Before Mailcow binds its ports, confirm nothing else is using them:

sudo ss -tlpn | grep -E -w '25|80|110|143|443|465|587|993|995|4190' || true

Clone the stable project:

cd /opt
sudo git clone https://github.com/mailcow/mailcow-dockerized
cd /opt/mailcow-dockerized
sudo ./generate_config.sh

Enter home.packet.init.wtf as MAILCOW_HOSTNAME. Review /opt/mailcow-dockerized/mailcow.conf and set:

MAILCOW_HOSTNAME=home.packet.init.wtf
TZ=America/New_York
HTTP_REDIRECT=y

On a 16 GB Pi, keep ClamAV and full-text search enabled initially. On an 8 GB Pi sharing resources with RabbitMQ, use:

SKIP_CLAMD=y
SKIP_FTS=y

Those switches reduce protection and search capability in exchange for memory. Record the decision in the build log.

Certificate choice

Mailcow’s normal ACME mode uses HTTP-01 and needs public TCP 80 to reach the Pi. If you deliberately expose home.packet.init.wtf, forward TCP 80 and 443 to the Pi and let Mailcow validate the name.

For the recommended VPN-oriented build, use Mailcow’s DNS-01 integration instead. It can issue a certificate without exposing port 80, but the exact API credentials depend on the authoritative DNS provider. Follow Mailcow’s current DNS-01 page and grant the token only the DNS permissions it needs. Do not paste DNS API credentials into the zine.

Start Mailcow:

cd /opt/mailcow-dockerized
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps

Open the admin UI only from the trusted LAN or VPN. Change the default administrator password immediately and enable two-factor authentication.

Create:

  • domain: packet.init.wtf

  • mailbox: packetbridge@packet.init.wtf

  • alias: red@packet.init.wtf → packetbridge@packet.init.wtf

  • alias: green@packet.init.wtf → packetbridge@packet.init.wtf

  • mailbox: dmarc@packet.init.wtf, kept separate from the bridge

Use unique password-manager-generated credentials. Do not enable a catch-all address.

Create a DKIM key for packet.init.wtf in the Mailcow UI and retain the exact TXT record for the DNS phase.

Phase 5 — Publish the right DNS

Recommended Mode B records

mx1.packet.init.wtf.   A      YOUR_VPS_STATIC_IP
packet.init.wtf.       MX 10  mx1.packet.init.wtf.
home.packet.init.wtf.  CNAME  initmail.ddns.net.
packet.init.wtf.       TXT    "v=spf1 a:mx1.packet.init.wtf -all"

Add the DKIM TXT value generated by Mailcow. Begin DMARC in observation mode while testing:

_dmarc.packet.init.wtf. TXT "v=DMARC1; p=none; rua=mailto:dmarc@packet.init.wtf"

After SPF, DKIM, forwarding, and legitimate message flows pass repeatedly, move toward p=quarantine and eventually p=reject. Do not begin at reject while the routing path is still changing.

At the VPS provider, set:

PTR YOUR_VPS_STATIC_IP -> mx1.packet.init.wtf

Forward and reverse names should agree:

dig +short mx1.packet.init.wtf A
dig -x YOUR_VPS_STATIC_IP +short
dig +short packet.init.wtf MX
dig +short packet.init.wtf TXT

Direct Mode A records

If every residential gate passed, the MX may point to a No-IP hostname that resolves directly to the home address:

packet.init.wtf. MX 10 initmail.ddns.net.

The ISP must still set PTR to the SMTP hostname, permit port 25, and tolerate the service. A correct forward record cannot compensate for missing reverse DNS.

Phase 6 — Build the WireGuard mail tunnel

Mode B uses a point-to-point tunnel. The home Pi initiates it, so CGNAT and a changing residential address do not prevent the VPS from replying through the established mapping.

Install WireGuard on the VPS and Pi:

sudo apt update
sudo apt install -y wireguard

Generate keys separately on each host:

sudo -i
umask 077
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
exit

Never copy a private key into chat, a screenshot, the zine, or Git.

VPS /etc/wireguard/wg0.conf

[Interface]
Address = 10.77.0.1/24
ListenPort = 51820
PrivateKey = <VPS_PRIVATE_KEY>

[Peer]
PublicKey = <PI_PUBLIC_KEY>
AllowedIPs = 10.77.0.2/32

Pi /etc/wireguard/wg0.conf

[Interface]
Address = 10.77.0.2/24
PrivateKey = <PI_PRIVATE_KEY>

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = mx1.packet.init.wtf:51820
AllowedIPs = 10.77.0.1/32
PersistentKeepalive = 25

PersistentKeepalive = 25 keeps the home NAT mapping alive. The No-IP hostname is not required for this tunnel because the stable VPS is the endpoint and the Pi initiates the connection.

Protect and start the interface on both hosts:

sudo chmod 600 /etc/wireguard/wg0.conf /etc/wireguard/private.key
sudo systemctl enable --now wg-quick@wg0
sudo wg show

Test from the Pi:

ping -c 3 10.77.0.1

Test from the VPS:

ping -c 3 10.77.0.2

Permit UDP 51820 on the VPS firewall. Do not forward UDP 51820 on the home router for this topology; the Pi is the initiating peer.

Phase 7 — Configure the VPS as a narrow SMTP edge

This Postfix edge accepts mail only for the listed lab recipients and accepts outbound relay only from the Pi’s WireGuard address.

Install:

sudo apt update
sudo apt install -y postfix postfix-pcre certbot swaks

Choose “Internet Site” and enter mx1.packet.init.wtf when the package asks.

Add or reconcile these settings in /etc/postfix/main.cf:

myhostname = mx1.packet.init.wtf
myorigin = $myhostname
inet_interfaces = all
inet_protocols = ipv4
mydestination = localhost

mynetworks = 127.0.0.0/8 10.77.0.2/32
relay_domains = packet.init.wtf
relay_recipient_maps = hash:/etc/postfix/relay_recipients
transport_maps = hash:/etc/postfix/transport

smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination
smtp_send_xforward_command = yes

smtpd_tls_security_level = may
smtp_tls_security_level = may

List only real recipients in /etc/postfix/relay_recipients:

packetbridge@packet.init.wtf OK
red@packet.init.wtf          OK
green@packet.init.wtf        OK
dmarc@packet.init.wtf        OK

Route the domain through WireGuard in /etc/postfix/transport:

packet.init.wtf smtp:[10.77.0.2]:25

Compile the maps and check syntax:

sudo postmap /etc/postfix/relay_recipients
sudo postmap /etc/postfix/transport
sudo postfix check
sudo systemctl restart postfix
sudo systemctl status postfix --no-pager

Acquire a public certificate for mx1.packet.init.wtf using the appropriate Certbot HTTP or DNS method, then set:

smtpd_tls_cert_file = /etc/letsencrypt/live/mx1.packet.init.wtf/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mx1.packet.init.wtf/privkey.pem

Reload Postfix after the certificate exists:

sudo postfix check
sudo systemctl reload postfix

At the VPS firewall allow TCP 25 and UDP 51820. TCP 80 is needed if Certbot uses HTTP-01. Do not open RabbitMQ ports on the VPS.

In Mailcow, add 10.77.0.1/32 under Configuration & Details → Forwarding Hosts so its Postfix instance can accept XFORWARD metadata only from the WireGuard edge. Then inspect received headers and spam-filter behavior. Trusting a forwarding host changes where filtering responsibility sits; do not assume the thin VPS edge provides the same protection as a mature filtering gateway.

Prove it is not an open relay

From a machine outside the VPS and outside the WireGuard tunnel:

swaks --server mx1.packet.init.wtf \
  --from outsider@example.net \
  --to unrelated-recipient@gmail.com

The VPS must reject relaying. Then test a known lab recipient:

swaks --server mx1.packet.init.wtf \
  --from your-external-test-address@example.net \
  --to red@packet.init.wtf

The second test should be accepted by the edge and delivered to Mailcow through 10.77.0.2. If the first test succeeds, stop Postfix immediately and fix the relay restrictions.

Send Mailcow outbound through the edge

In Mailcow’s admin UI:

  • Open Configuration & Details → Routing.

  • Add [10.77.0.1]:25 as a relayhost/smarthost.

  • No SMTP password is needed because WireGuard authenticates the peer and Postfix trusts only 10.77.0.2/32.

  • Assign that relayhost to the packet.init.wtf domain.

  • Run Mailcow’s relayhost test.

Docker normally masquerades traffic leaving its bridge, so the VPS should see the connection as 10.77.0.2. Confirm in the VPS mail log. If it sees another source address, do not widen mynetworks; correct the Pi’s Docker-to-WireGuard routing instead.

Phase 8 — Install RabbitMQ and the bridge

The accompanying packetmail-bridge code bundle contains:

  • .env.example — secret-free configuration template

  • Dockerfile and compose.yaml — isolated runtime

  • package.json and package-lock.json — pinned Node dependencies

  • src/bridge.mjs and src/lib.mjs — bridge and validation logic

  • test/lib.test.mjs — deterministic unit tests

  • LICENSE — MIT terms for the original bridge code

Copy it to the Pi as /opt/packetmail-bridge. The source is deliberately outside /opt/mailcow-dockerized; Mailcow upgrades own their directory and our bridge owns its own.

Create the runtime environment:

cd /opt/packetmail-bridge
sudo cp .env.example .env
sudo chmod 600 .env
openssl rand -hex 24
openssl rand -hex 32
sudo nano .env

Use the first generated value for both RABBITMQ_PASSWORD and the password inside RABBITMQ_URL. Hex characters are URL-safe. Use the second for PACKET_HMAC_SECRET.

Fill in the Mailcow credentials for packetbridge@packet.init.wtf. The example environment deliberately limits outbound mail to packet.init.wtf:

OUTBOUND_ALLOWED_DOMAINS=packet.init.wtf

Do not widen that list until abuse controls, rate limits, logs, and the public-mail path have been tested.

Build and test the code:

cd /opt/packetmail-bridge
sudo docker build --target test -t packetmail-bridge:test .

Start the services:

sudo docker compose build
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail 100 bridge

The bundle pins RabbitMQ 4.3.4-management, the current release when this manual was drafted. Before deploying later, compare the pinned version with RabbitMQ’s supported releases and test upgrades deliberately.

What the bridge declares

Exchange/queue Type Purpose
packetmail topic exchange Normal routing
packetmail.dlx topic exchange Failure routing
node.red.inbox durable queue Red node mailbox
node.green.inbox durable queue Green node mailbox
packetmail.outbound durable queue Requests to send mail
packetmail.retry.60s delayed queue Finite outbound retries
packetmail.dead durable queue Final failures and rejected mail

Inbound messages have a seven-day queue TTL and a 10,000-envelope cap. Raw messages larger than 1 MiB go to the Dead Letter Office. Parsed text is limited to 16 KiB. Attachments remain in Mailcow; RabbitMQ receives only attachment metadata.

Management without public exposure

RabbitMQ management is bound to loopback on the Pi. Reach it through SSH:

ssh -L 15672:127.0.0.1:15672 youradmin@192.168.50.20

Then open http://127.0.0.1:15672. Never forward 15672 or 5672 from the home router.

For a hardened revision, create a separate administrator account and remove administrator tags from the bridge account. The first lab keeps one loopback-only account to reduce bootstrap complexity.

Phase 9 — Understand the envelope

Email to red@packet.init.wtf becomes:

{
  "version": 1,
  "packet_id": "c4c6f6ecfddbf3a5fd02a863301ff32f",
  "source": "sender@example.net",
  "destination": "node.red",
  "created_at": "2026-08-11T16:00:00.000Z",
  "received_at": "2026-08-11T16:00:02.000Z",
  "message_id": "<original-message-id@example.net>",
  "subject": "TRANSMISSION 0042",
  "text": "The compact plain-text body.",
  "raw_sha256": "...",
  "raw_bytes": 4812,
  "imap_uid": 184,
  "attachments": [],
  "signature": "..."
}

The deterministic packet ID combines the original message identity, raw-message hash, and destination. Consumers can store processed packet IDs and safely ignore redeliveries.

The HMAC detects accidental or unauthorized changes inside the trusted packet network. It does not encrypt the envelope. Keep RabbitMQ on the host network boundary or a VPN and add AMQP TLS before separating nodes across untrusted links.

Nothing in an email body is executed. The bridge emits data, not shell commands.

Phase 10 — Test the whole chain

Test A: infrastructure

On the Pi:

sudo docker ps
sudo docker compose -f /opt/mailcow-dockerized/docker-compose.yml ps
sudo docker compose -f /opt/packetmail-bridge/compose.yaml ps
sudo wg show
getent ahostsv4 initmail.ddns.net

On the VPS:

sudo wg show
sudo postfix check
sudo postqueue -p
sudo journalctl -u postfix -n 100 --no-pager

Test B: public inbound

Send a plain-text email from an unrelated provider to red@packet.init.wtf.

Verify in order:

  • VPS Postfix accepted the known recipient.

  • Mailcow shows the original in the red mailbox or the dedicated bridge mailbox chosen by your addressing design.

  • Bridge logs show inbound_processed.

  • RabbitMQ shows one ready message in node.red.inbox.

The supplied bridge polls packetbridge@packet.init.wtf. The red and green Mailcow aliases deliver into that one source mailbox while the visible To header retains the node address used for RabbitMQ routing. This first bridge intentionally does not route BCC-only recipients because their destination is absent from the message headers.

Test C: offline delivery

Leave node.red.inbox without a consumer. Send three messages. Confirm the ready count reaches three. Start a consumer and acknowledge them one at a time. This is the project’s central demonstration: the node can be absent without losing its mail.

Test D: outbound

Publish this JSON to exchange packetmail with routing key mail.outbound:

{
  "to": "green@packet.init.wtf",
  "subject": "RETURN PATH TEST",
  "text": "The rabbit has left the sorting office."
}

The bridge submits it to Mailcow over authenticated port 587. Mailcow signs it and sends it through the VPS relayhost. The outbound worker retries twice through the 60-second retry queue; the third failure goes to packetmail.dead.

Test E: the Dead Letter Office

Perform three controlled failures:

  • Send to unknown@packet.init.wtf after temporarily adding it as a Mailcow alias but not adding it to PACKET_NODES.

  • Send a message larger than MAX_RAW_BYTES.

  • Publish an outbound request to a domain absent from OUTBOUND_ALLOWED_DOMAINS.

All three should be visible as dead-letter events without executing content or creating an infinite retry loop.

Failure atlas

No-IP resolves to the wrong address

  • Compare the No-IP dashboard with the router WAN address.

  • Inspect docker logs noip-duc.

  • Confirm the DDNS Key covers the selected hostname.

  • Do not update more frequently than the provider allows.

  • Remember that free hostname confirmation is separate from DUC updates.

External mail never reaches the VPS

  • Verify MX and A records.

  • Verify PTR separately.

  • Confirm TCP 25 is allowed by the VPS provider and firewall.

  • Inspect Postfix logs while sending a test.

  • Confirm the sender is addressing packet.init.wtf, not home.packet.init.wtf.

VPS accepts mail but the Pi never sees it

  • Check wg show for a recent handshake.

  • Ping 10.77.0.2 from the VPS.

  • Confirm Mailcow publishes TCP 25 on the Pi.

  • Check the Pi’s DOCKER-USER rules and router/LAN firewall.

  • Confirm /etc/postfix/transport was processed with postmap.

Mailcow receives mail but RabbitMQ stays empty

  • Confirm the message landed in packetbridge@packet.init.wtf.

  • Confirm the bridge IMAP password.

  • Confirm the To header still contains red@packet.init.wtf or green@packet.init.wtf.

  • Confirm PACKET_NODES=red,green.

  • Inspect bridge JSON logs; they never print passwords.

RabbitMQ receives duplicate envelopes

At-least-once processing permits redelivery when a process fails between publish confirmation and marking the IMAP message seen. Deduplicate by packet_id; do not pretend exactly-once delivery exists across IMAP and AMQP without a transactional boundary.

Outbound mail loops or retries forever

The supplied worker does not requeue the same delivery immediately. It republishes into a 60-second retry queue and stops after the configured attempt limit. If a custom consumer calls nack(requeue=true) forever, it has removed one of the design’s safety properties.

Mail lands in spam

  • Verify PTR, SPF, DKIM, and DMARC alignment.

  • Inspect the receiving provider’s authentication results.

  • Check the VPS IP’s reputation.

  • Send normal, low-volume test traffic.

  • Do not use the lab for bulk mail.

Security doctrine

  • Keep the experiment on a subdomain.

  • Never publish credentials, WireGuard private keys, DKIM private keys, or Mailcow encryption keys.

  • Do not expose RabbitMQ AMQP or management ports to the internet.

  • Do not execute attachments, HTML, URLs, headers, or body text.

  • Keep a hard message-size ceiling.

  • Allowlist outbound domains during the lab.

  • Reject unknown recipients at the VPS before accepting their message bodies.

  • Use finite retries and a visible dead-letter queue.

  • Treat every consumer as idempotent.

  • Use unique service accounts and two-factor authentication where available.

  • Keep the Pi and VPS patched.

  • Review logs without retaining more personal content than necessary.

  • Do not turn the system into an anonymous relay, bulk sender, or covert tunnel.

This machine processes correspondence. Even a playful lab inherits privacy obligations.

Backup and recovery

Mailcow stores mail, databases, spam-learning state, and encryption material in Docker volumes. Its encryption key material is essential; a backup without the keys is not a recovery plan.

Use Mailcow’s supported helper:

cd /opt/mailcow-dockerized
sudo ./helper-scripts/backup_and_restore.sh backup all --delete-days 14

Back up separately:

  • /opt/mailcow-dockerized/mailcow.conf

  • /opt/noip-duc/noip-duc.env

  • /etc/wireguard/

  • /opt/packetmail-bridge/.env

  • RabbitMQ durable data or exported definitions

  • VPS Postfix configuration and maps

  • DNS record inventory

Encrypt backup media. Test a restore on an isolated machine. A backup job that has never completed a restore test is only a theory.

Maintenance calendar

Weekly

  • confirm the WireGuard handshake is recent

  • inspect Mailcow and bridge health

  • check the Dead Letter Office

  • check disk and backup capacity

Monthly

  • apply operating-system security updates

  • run Mailcow’s supported update process

  • review the pinned RabbitMQ release against current support

  • verify SPF, DKIM, DMARC, MX, A, and PTR

  • test inbound and outbound delivery from an external provider

  • confirm the No-IP hostname if using the free tier

  • restore one small artifact from backup

After every topology change

  • repeat the open-relay test

  • repeat the unknown-recipient test

  • repeat the offline-node test

  • confirm no new Docker port is public

Zine treatment

This article should look like a recovered postal-computing manual:

  • dot-matrix route slips instead of cards

  • tractor-feed edges on only the configuration spreads

  • one full-page topology diagram stamped NOT TO SCALE

  • green-and-black photographs of the Pi, SSD, and Ethernet switch

  • a redacted Postfix queue listing

  • a redacted RabbitMQ queue screenshot

  • physical rubber-stamp marks for ACK, NACK, RETRY, and DEAD

  • a fold-out DEAD LETTER MANIFEST containing the three controlled failures

Suggested cover line:

RUN A POST OFFICE INSIDE YOUR POST OFFICE

Suggested pull quote:

Dynamic DNS tells the world where the house moved. It does not make the neighborhood trust your mail.

Definition of done

Do not label the article FIELD TESTED until every box is checked:

  • [ ] Pi boots from SSD after power loss

  • [ ] UPS shutdown or recovery behavior tested

  • [ ] No-IP changes propagate correctly

  • [ ] Mailcow certificate validates without warnings

  • [ ] External MX, A, and PTR agree

  • [ ] SPF passes

  • [ ] DKIM passes

  • [ ] DMARC report arrives

  • [ ] VPS refuses unrelated relay

  • [ ] WireGuard reconnects after home IP change

  • [ ] Known recipient reaches Mailcow

  • [ ] Mailcow message becomes a RabbitMQ envelope

  • [ ] Offline node retains three messages

  • [ ] Consumer deduplicates a replayed packet ID

  • [ ] Oversized input reaches the Dead Letter Office

  • [ ] Outbound failure stops after finite retries

  • [ ] Mailcow backup completes

  • [ ] Isolated restore succeeds

  • [ ] Every photographed secret is redacted

Until then, the correct cover stamp is LAB-READY DRAFT.

Primary references

  • Mailcow system requirements and ports — https://docs.mailcow.email/getstarted/prerequisite-system/

  • Mailcow installation — https://docs.mailcow.email/getstarted/install/

  • Mailcow DNS requirements — https://docs.mailcow.email/getstarted/prerequisite-dns/

  • Mailcow relayhosts — https://docs.mailcow.email/manual-guides/Postfix/u_e-postfix-relayhost/

  • Mailcow external-forwarding example — https://docs.mailcow.email/third_party/exchange_onprem/third_party-exchange_onprem/

  • Mailcow DNS-01 certificates — https://docs.mailcow.email/post_installation/firststeps-ssl-dns/

  • Mailcow backups — https://docs.mailcow.email/backup_restore/b_n_r-backup/

  • No-IP Linux Dynamic Update Client — https://www.noip.com/support/knowledgebase/install-linux-3-x-dynamic-update-client-duc

  • No-IP DUC Docker deployment — https://www.noip.com/support/knowledgebase/ip-linux-duc-docker

  • No-IP free-hostname confirmation — https://www.noip.com/support/knowledgebase/confirm-my-hostname-free-account-support-question-day

  • WireGuard quick start and persistent keepalive — https://www.wireguard.com/quickstart/

  • RabbitMQ current installation guidance — https://www.rabbitmq.com/docs/download

  • RabbitMQ publisher confirms and consumer acknowledgements — https://www.rabbitmq.com/docs/confirms

  • RabbitMQ dead-letter exchanges — https://www.rabbitmq.com/docs/dlx

  • Docker Engine on Ubuntu — https://docs.docker.com/engine/install/ubuntu/

END OF LINE // INIT.WTF // FIELD MANUAL 02