diff --git a/.env b/.env deleted file mode 100644 index 4e2448a..0000000 --- a/.env +++ /dev/null @@ -1,21 +0,0 @@ -# Mail Room version -VERSION=dev - -### -# Network binds -### -INBOX_BIND=0.0.0.0:25 - -OUTBOX_BIND=0.0.0.0:587 - -DOVECOT_BIND=0.0.0.0:993 - -POSTNET_NETWORK=172.22.0 - -### -# Directories -### - -CERT_PATH=./certs - -CONF_PATH=./configs \ No newline at end of file diff --git a/.env b/.env new file mode 120000 index 0000000..5cbb5c6 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +mailroom.env \ No newline at end of file diff --git a/conf/apparmor/mailroom-inbox b/conf/apparmor/mailroom-inbox index 47ef786..1a0bcae 100644 --- a/conf/apparmor/mailroom-inbox +++ b/conf/apparmor/mailroom-inbox @@ -36,11 +36,10 @@ profile mailroom-inbox flags=(attach_disconnected, mediate_deleted) { /sys/fs/cgroup/** r, # cgroups (read-only) ## Capabilities and signals - capability dac_override, # Required capability for file access + capability net_bind_service # Required capability to bind to port 25 signal (receive) peer=unconfined, # Allow signals from unconfined processes network inet stream, network inet6 dgram, network inet6 stream, network netlink raw, - deny network, } diff --git a/inbox/Dockerfile b/inbox/Dockerfile index e655230..fffd041 100644 --- a/inbox/Dockerfile +++ b/inbox/Dockerfile @@ -1,26 +1,28 @@ FROM node:20 AS build COPY . /app WORKDIR /app - +RUN rm entrypoint.sh RUN npm i --omit=dev FROM node:20-alpine -ENV INBOX_PORT="25" -ENV INBOX_HOST="smtp.example.com" -ENV INBOX_LOG_FILE="/tmp/inbox.log" -ENV INBOX_TLS_MIN_VERSION="TLSv1.3" -ENV INBOX_TLS_KEY_PATH="/certs/inbox/privkey.pem" -ENV INBOX_TLS_CERT_PATH="/certs/inbox/cert.pem" +ENV LOG_FILE="/var/log/inbox.log" +ENV LOG_LEVEL="INFO" +ENV INBOX_COINTAINER_TLS_KEY="/etc/ssl/inbox/privkey.pem" +ENV INBOX_COINTAINER_TLS_CERT="/etc/ssl/inbox/cert.pem" ENV REDIS_HOST="redis_mail" -ENV REDIS_PORT="6379" +ENV REDIS_PORT=6379 +ENV INBOX_MAX_CONNECTIONS=1024 +ENV INBOX_PORT=25 -RUN apk add openssl; adduser app -H -D +RUN apk add openssl WORKDIR /usr/src/inbox COPY --from=build /app /usr/src/inbox +COPY --chmod=500 --chown=1000 entrypoint.sh /entrypoint.sh +EXPOSE $INBOX_PORT -USER app - -ENTRYPOINT "node src/index.js 2>&1 | tee /tmp/inbox.log" \ No newline at end of file +USER 1000 +ENTRYPOINT ["/entrypoint.sh"] +CMD ["src/index.js"] \ No newline at end of file diff --git a/inbox/Dockerfile.dev b/inbox/Dockerfile.dev index e74a67c..48091cd 100644 --- a/inbox/Dockerfile.dev +++ b/inbox/Dockerfile.dev @@ -1,27 +1,30 @@ FROM node:20 AS build COPY . /app WORKDIR /app - +RUN rm entrypoint.sh RUN npm i --omit=dev FROM node:20-alpine -ENV INBOX_PORT="25" -ENV INBOX_HOST="smtp.example.com" -ENV INBOX_LOG_FILE="/tmp/inbox.log" -ENV INBOX_TLS_MIN_VERSION="TLSv1.3" -ENV INBOX_TLS_KEY_PATH="/certs/inbox/privkey.pem" -ENV INBOX_TLS_CERT_PATH="/certs/inbox/cert.pem" +ENV LOG_FILE="/var/log/inbox.log" +ENV LOG_LEVEL="INFO" +ENV INBOX_COINTAINER_TLS_KEY="/etc/ssl/inbox/privkey.pem" +ENV INBOX_COINTAINER_TLS_CERT="/etc/ssl/inbox/cert.pem" ENV REDIS_HOST="redis_mail" -ENV REDIS_PORT="6379" +ENV REDIS_PORT=6379 +ENV INBOX_MAX_CONNECTIONS=1024 +ENV INBOX_PORT=25 -RUN apk add openssl; adduser app -H -D +RUN apk add openssl WORKDIR /usr/src/inbox COPY --from=build /app /usr/src/inbox +COPY --chmod=500 --chown=1000 entrypoint.sh /entrypoint.sh COPY ./smtp-server /usr/src/inbox/node_modules/@carlgo11/smtp-server -USER app +EXPOSE $INBOX_PORT -ENTRYPOINT "node src/index.js 2>&1 | tee /tmp/inbox.log" \ No newline at end of file +USER 1000 +ENTRYPOINT ["/entrypoint.sh"] +CMD ["src/index.js"] \ No newline at end of file diff --git a/inbox/entrypoint.sh b/inbox/entrypoint.sh new file mode 100644 index 0000000..c4fa1ed --- /dev/null +++ b/inbox/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +npm run test && +node "$*" 2>&1 | tee "$LOG_FILE" \ No newline at end of file diff --git a/inbox/package.json b/inbox/package.json index e3fd59a..b9efeac 100644 --- a/inbox/package.json +++ b/inbox/package.json @@ -24,11 +24,8 @@ "private": true, "license": "LGPL-3.0-or-later", "scripts": { - "start": "node --env-file=../.env src/index.js", - "test": "node --test", - "lint": "eslint src --ext .js,.mjs", - "dev": "node src/index.js --watch-path src/", - "prepublishOnly": "npm test && npm run lint" + "start": "node src/index.js", + "test": "node --test" }, "dependencies": { "mailauth": "^4.6.8", diff --git a/inbox/src/config/tls.js b/inbox/src/config/tls.js index ad616da..d66d281 100644 --- a/inbox/src/config/tls.js +++ b/inbox/src/config/tls.js @@ -1,10 +1,9 @@ import fs from 'fs'; export const tlsConfig = { - key: fs.readFileSync(process.env.INBOX_TLS_KEY_PATH || process.env.TLS_KEY_PATH), - cert: fs.readFileSync(process.env.INBOX_TLS_CERT_PATH || process.env.TLS_CERT_PATH), + key: fs.readFileSync(process.env.INBOX_COINTAINER_TLS_KEY), + cert: fs.readFileSync(process.env.INBOX_COINTAINER_TLS_CERT), minVersion: process.env.INBOX_TLS_MIN_VERSION || process.env.TLS_MIN_VERSION, maxVersion: process.env.INBOX_TLS_MAX_VERSION || process.env.TLS_MAX_VERSION, ciphers: process.env.INBOX_TLS_CIPHERS || process.env.TLS_CIPHERS, - handshakeTimeout: 5000, }; diff --git a/inbox/src/servers/server.js b/inbox/src/servers/server.js index 6562cb2..2a1c606 100644 --- a/inbox/src/servers/server.js +++ b/inbox/src/servers/server.js @@ -21,13 +21,13 @@ export default function startServer() { tlsOptions, extensions: ['ENHANCEDSTATUSCODES', 'PIPELINING', 'REQUIRETLS', '8BITMIME'], greeting: process.env.INBOX_HOST, - onRCPTTO: async (address, session) => await handleRcptTo(address, session), - onMAILFROM: async (address, session, ext) => await handleMailFrom(address, session, ext), - onEHLO: async (domain, session) => await handleEhlo(domain, session), - onDATA: async (message, session) => await handleData(message, session), - onConnect: async(session) => await handleConnect(session), - logLevel: process.env.LOG_LEVEL || 'INFO', - maxConnections: process.env.INBOX_MAX_CONNECTIONS || 100, + onRCPTTO: handleRcptTo, + onMAILFROM: handleMailFrom, + onEHLO: handleEhlo, + onDATA: handleData, + onConnect: handleConnect, + logLevel: process.env.LOG_LEVEL, + maxConnections: process.env.INBOX_MAX_CONNECTIONS, }); } catch (e) { console.error(e); diff --git a/inbox/test/email.test.js b/inbox/test/email.test.js deleted file mode 100644 index 5bbdffd..0000000 --- a/inbox/test/email.test.js +++ /dev/null @@ -1,147 +0,0 @@ -import {describe, it} from 'node:test'; -import {Readable} from 'node:stream'; -import assert from 'node:assert'; -import Email from '../src/models/email.js'; - -// Helper function to create a stream from a string -function createStreamFromString(str) { - return new Readable({ - read() { - this.push(str); - this.push(null); // End the stream - }, - }); -} - -describe('Email.parseHeaders', () => { - it('should correctly parse standard headers', () => { - const email = new Email(); - const headers = `Subject: Test Email\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n`; - - email.parseHeaders(headers); - - assert.strictEqual(email.headers.subject, 'Test Email'); - assert.strictEqual(email.headers.from, 'sender@example.com'); - assert.strictEqual(email.headers.to, 'recipient@example.com'); - }); - - it('should handle folded headers correctly', () => { - const email = new Email(); - const headers = `Folded-Header: This is a\r\n folded header\r\n`; - - email.parseHeaders(headers); - - assert.strictEqual(email.headers['folded-header'], - 'This is a folded header'); - }); - - it('should handle duplicate headers by storing them in an array', () => { - const email = new Email(); - const headers = `X-Custom-Header: Value1\r\nX-Custom-Header: Value2\r\n`; - - email.parseHeaders(headers); - - assert.deepStrictEqual(email.headers['x-custom-header'], - ['Value1', 'Value2']); - }); - - it('should ignore empty or invalid header lines', () => { - const email = new Email(); - const headers = `Subject: Test\r\nInvalidHeaderWithoutColon\r\nTo: recipient@example.com\r\n`; - - email.parseHeaders(headers); - - // Check that the valid headers are parsed correctly - assert.strictEqual(email.headers.subject, 'Test'); - assert.strictEqual(email.headers.to, 'recipient@example.com'); - - // The invalid header line should not be present - assert.strictEqual(email.headers['InvalidHeaderWithoutColon'], undefined); - }); -}); - -describe('Email.parseStream', () => { - it('should parse headers and body correctly', async () => { - const email = new Email(); - const rawEmail = `Subject: Test Email\r\nFrom: sender@example.com\r\n\r\nThis is the body of the email.`; - - const stream = createStreamFromString(rawEmail); - - // console.log('stream',stream); - await email.parseMessage(stream); - - assert.strictEqual(email.headers.subject, 'Test Email'); - assert.strictEqual(email.headers.from, 'sender@example.com'); - assert.strictEqual(email.body, 'This is the body of the email.'); - }); - - it('should handle a large body streamed in chunks', async () => { - const email = new Email(); - const headers = `Subject: Large Email\r\nFrom: sender@example.com\r\n\r\n`; - const body = 'This is a large body. '.repeat(1000); // Repeating to simulate a large body - const rawEmail = headers + body; - - const stream = createStreamFromString(rawEmail); - - await email.parseMessage(stream); - - assert.strictEqual(email.headers.subject, 'Large Email'); - assert.strictEqual(email.headers.from, 'sender@example.com'); - assert.strictEqual(email.body, body); - }); - - it('should parse the subject correctly from the body using simpleParser', - async () => { - const email = new Email(); - const rawEmail = `Subject: Test Email Subject\r\nFrom: sender@example.com\r\n\r\nBody content.`; - - const stream = createStreamFromString(rawEmail); - - await email.parseMessage(stream); - - assert.strictEqual(email.subject, 'Test Email Subject'); - }); - - it('should handle stream errors properly', async () => { - const email = new Email(); - const stream = new Readable({ - read() { - this.emit('error', new Error('Stream error')); - }, - }); - - try { - await email.parseMessage(stream); - assert.fail('parseStream did not reject as expected'); - } catch (err) { - assert.strictEqual(err.message, - 'Error processing incoming email: Stream error'); - } - }); - - it('should handle folded headers correctly', async () => { - const email = new Email(); - const rawEmail = `Subject: Folded\r\n header\r\nFrom: sender@example.com\r\n\r\nBody content.`; - - const stream = createStreamFromString(rawEmail); - - await email.parseMessage(stream); - - assert.strictEqual(email.headers.subject, 'Folded header'); - assert.strictEqual(email.headers.from, 'sender@example.com'); - assert.strictEqual(email.body, 'Body content.'); - }); -}); - -describe('Email.headers', () => { - it('should retrieve header as expected', () => { - const email = new Email(); - const headers = 'headera: lowercase header\r\nHEADERB: UPPERCASE HEADER\r\n\r\n'; - email.parseHeaders(headers); - email.addHeader('HeAdErC', 'Mixed Case Header'); - - assert.strictEqual(email.getHeader('HEADERA'), 'lowercase header'); - assert.strictEqual(email.headers.headerb, 'UPPERCASE HEADER'); - assert.strictEqual(email.headers['headerc'], 'Mixed Case Header'); - }); -}); diff --git a/installation/compose/backup b/installation/compose/backup index 3e069f2..bd1590c 100644 --- a/installation/compose/backup +++ b/installation/compose/backup @@ -1,7 +1,6 @@ backup: container_name: backup image: carlgo11/mailroom-backup:${VERSION:-dev} - env_file: mailroom.env tmpfs: - /tmp read_only: true diff --git a/installation/compose/dovecot b/installation/compose/dovecot index 3c64f2a..9634c2b 100644 --- a/installation/compose/dovecot +++ b/installation/compose/dovecot @@ -2,13 +2,13 @@ container_name: dovecot image: dovecot/dovecot ports: - - "${DOVECOT_BIND:-993}:993/tcp" + - "${DOVECOT_IPV4_BIND:-0.0.0.0:993}:993/tcp" + - "${DOVECOT_IPV6_BIND:-:::993}:993/tcp" volumes: - ${CONF_PATH:-./configs}/dovecot:/etc/dovecot/conf.d:ro - - ${CERT_PATH:-./certs}/dovecot:/certs/dovecot:ro - - ${CERT_PATH:-./certs}/clients:/certs/clients:ro + - ${DOVECOT_TLS_KEY}:/certs/dovecot/privkey.pem:ro + - ${DOVECOT_TLS_CERT}:/certs/dovecot/cert.pem:ro - vhosts:/var/mail/vhosts - env_file: mailroom.env depends_on: - redis networks: diff --git a/installation/compose/inbox b/installation/compose/inbox index 86940a2..11abcf4 100644 --- a/installation/compose/inbox +++ b/installation/compose/inbox @@ -3,13 +3,18 @@ image: carlgo11/mailroom-inbox:${VERSION:-dev} pull_policy: always ports: - - "${INBOX_BIND:-25}:25/tcp" + - "${INBOX_IPV4_BIND:-0.0.0.0:25}:25/tcp" + - "${INBOX_IPV6_BIND:-:::25}:25/tcp" volumes: - - ${CERT_PATH:-./certs}/inbox:/certs/inbox:ro - - ${CERT_PATH:-./certs}/clients/users:/certs/clients/users:ro + - ${INBOX_TLS_KEY}:/etc/ssl/inbox/privkey.pem + - ${INBOX_TLS_CERT}:/etc/ssl/inbox/cert.pem + - ${INBOX_SMIME_PATH}:/etc/ssl/clients/:ro + - ${INBOX_LOG}:/var/log/inbox.log - vhosts:/var/mail/vhosts - - /var/log/inbox.log:/tmp/inbox.log - env_file: mailroom.env + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE depends_on: - redis networks: diff --git a/installation/compose/outbox b/installation/compose/outbox index 4b6341f..d6ba5f5 100644 --- a/installation/compose/outbox +++ b/installation/compose/outbox @@ -2,12 +2,13 @@ container_name: outbox image: carlgo11/mailroom-outbox:${VERSION:-dev} ports: - - "${OUTBOX_BIND:-587}:587" + - "${OUTBOX_IPV4_BIND:-0.0.0.0:587}:587/tcp" + - "${OUTBOX_IPV6_BIND:-:::587}:587/tcp" volumes: - - ${CERT_PATH:-./certs}/outbox:/certs/outbox:ro - - ${CERT_PATH:-./certs}/dkim:/certs/dkim:ro + - ${OUTBOX_TLS_KEY}:/etc/ssl/outbox/privkey.pem:ro + - ${OUTBOX_TLS_CERT}:/etc/ssl/outbox/cert.pem:ro + - ${OUTBOX_DKIM_PATH}:/etc/ssl/dkim:ro - vhosts:/var/mail/vhosts - env_file: mailroom.env depends_on: - redis read_only: true diff --git a/installation/compose/rspamd b/installation/compose/rspamd index 063b156..ba402c2 100644 --- a/installation/compose/rspamd +++ b/installation/compose/rspamd @@ -4,7 +4,6 @@ volumes: - rspamd_confdir:/etc/rspamd - rspamd_dbdir:/var/lib/rspamd - env_file: .env environment: - RSPAMD_REDIS_SERVERS=rspamd-compose-redis depends_on: diff --git a/mailroom.env b/mailroom.env index b6405a5..ae72a89 100644 --- a/mailroom.env +++ b/mailroom.env @@ -1,92 +1,110 @@ -# Mailroom example configuration +# Mail Room Example Configuration -# ---------------------------- -# Outbox (Submission) Server Configuration -# ---------------------------- -OUTBOX_PORT=465 -OUTBOX_HOST=mail.example.com +# Mail Room version +VERSION=dev + +# Internal IPv4 network subnet +POSTNET_NETWORK=172.22.0 -# ---------------------------- -# Inbox (MX) Server Configuration -# ---------------------------- -INBOX_PORT=25 +# Path to the client certificates used S/MIME encrypt emails. +# If left blank, S/MIME encryption is disabled. +USER_CERTS_PATH=./certs/users/ + +# ----------------------------------- +# Inbox Server Configuration +# ----------------------------------- +# Hostname for the Inbox server. INBOX_HOST=smtp.example.com +# IP address and port to bind the Inbox server. +INBOX_IPV4_BIND=0.0.0.0:25 +INBOX_IPV6_BIND=:::25 + +# Maximum number of concurrent connections allowed. INBOX_MAX_CONNECTIONS=1024 -# Required authentication for incoming mail. -# Allowed values: spf dkim arc dmarc -# If left blank, authentication is optional. -INBOX_AUTH="spf dkim dmarc" +# Specifies required authentication methods for incoming mail. +# Allowed values: spf dkim arc dmarc. If blank, authentication is optional. +INBOX_AUTH="" -# ---------------------------- -# Global TLS Configuration -# ---------------------------- -TLS_CIPHERS=TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 -TLS_MIN_VERSION=TLSv1.2 -TLS_MAX_VERSION=TLSv1.3 +# Path to the TLS private key for the Inbox server. +INBOX_TLS_KEY=/etc/ssl/inbox/privkey.pem +# Path to the TLS certificate for the Inbox server. +INBOX_TLS_CERT=/etc/ssl/inbox/cert.pem + +# Log path +INBOX_LOG=/tmp/inbox.log -# ---------------------------- -# Inbox TLS Configuration -# ---------------------------- -INBOX_TLS_KEY_PATH=/certs/inbox/privkey.pem -INBOX_TLS_CERT_PATH=/certs/inbox/cert.pem +# ----------------------------------- +# Outbox Server Configuration +# ----------------------------------- +# Hostname for the Outbox server. +OUTBOX_HOST=mail.example.com +# IP address and port to bind the Outbox server. +OUTBOX_IPV4_BIND=0.0.0.0:587 +OUTBOX_IPV6_BIND=:::587 + +# Path to the TLS private key for the Outbox server. +OUTBOX_TLS_KEY=/etc/ssl/outbox/privkey.pem +# Path to the TLS certificate for the Outbox server. +OUTBOX_TLS_CERT=/etc/ssl/outbox/cert.pem -# ---------------------------- -# Outbox TLS Configuration -# ---------------------------- -OUTBOX_TLS_KEY_PATH=/certs/outbox/privkey.pem -OUTBOX_TLS_CERT_PATH=/certs/outbox/cert.pem -OUTBOX_DKIM_PATH="/certs/dkim" +# Directory containing DKIM keys. +OUTBOX_DKIM_PATH=./certs/dkim/ -# ---------------------------- +# ----------------------------------- # Dovecot Configuration -# ---------------------------- -DOVECOT_TLS_CERT_PATH=/certs/dovecot/cert.pem -DOVECOT_TLS_KEY_PATH=/certs/dovecot/privkey.pem -DOVECOT_REQUIRE_CLIENT_CERT="yes" -DOVECOT_PORT=993 +# ----------------------------------- +# Hostname for the Dovecot IMAP server. DOVECOT_HOST=mail.example.com +# IP address and port to bind the Dovecot server. +DOVECOT_IPV4_BIND=0.0.0.0:993 +DOVECOT_IPV6_BIND=:::993 + +# Path to the TLS private key for Dovecot. +DOVECOT_TLS_KEY=/etc/ssl/dovecot/privkey.pem +# Path to the TLS certificate for Dovecot. +DOVECOT_TLS_CERT=/etc/ssl/dovecot/cert.pem + +# ----------------------------------- +# Global TLS Configuration +# ----------------------------------- +# Applies to both Inbox and Outbox services. +# Can be overridden by prefixing with INBOX_ or OUTBOX_. + +# List of supported TLS ciphers. +TLS_CIPHERS="TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256" +# Minimum allowed TLS version. +TLS_MIN_VERSION=TLSv1.3 +# Maximum allowed TLS version. +TLS_MAX_VERSION=TLSv1.3 -# ---------------------------- -# Client Certificate Configuration -# ---------------------------- -# If requiring client certificates for IMAP/Outbox, -# Specify the Certificate Authority and path for user certificates -CLIENT_CERT_PATH=/certs/clients/users/ -CLIENT_CERT_CA_CERT=/certs/clients/ca-cert.pem -CLIENT_CERT_CA_KEY=/certs/clients/ca-key.pem - -# ---------------------------- -# Mailbox Configuration -# ---------------------------- -MAILBOX_PATH=/var/mail/vhosts/ - -# ---------------------------- +# ----------------------------------- # Rspamd Configuration -# ---------------------------- +# ----------------------------------- +# Password for authenticating with the Rspamd service. RSPAMD_PASSWORD="" -# ---------------------------- +# ----------------------------------- # Spamhaus Configuration -# ---------------------------- -# Enter your Spamhaus API key if you have one. -# The integration will be disabled if left blank. +# ----------------------------------- +# API key for Spamhaus integration. +# The integration is disabled if API key is blank. SPAMHAUS_API_KEY="" -# ---------------------------- +# ----------------------------------- # IPQualityScore Configuration -# ---------------------------- -# Enter your IPQS API key if you have one. -# The integration will be disabled if left blank. +# ----------------------------------- +# API key for IPQualityScore integration. +# The integration is disabled if API key is blank. IPQS_API_KEY="" -# Max IPQS fraud score to accept. -# Connections from addresses with a higher score than the limit will be rejected. +# Defines the maximum acceptable IPQS fraud score. +# Connections with higher scores are rejected. IPQS_SCORE_LIMIT=90 -# ---------------------------- +# ----------------------------------- # IP-Score.com Configuration -# ---------------------------- -# IP-Score checks the connection address against multiple blacklists. -# No API key is required. +# ----------------------------------- +# Checks connection address against multiple blacklists. +# Enable or disable IP-Score.com integration. IPSCORE_ENABLED=true