From edc18c2c18e4665b141ef6acd8a413423713efa9 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 14 Feb 2025 20:48:15 -0500 Subject: [PATCH] create initial api (add, list endpoints) - NOT SECURE --- .gitignore | 4 ++ Dockerfile | 7 +++ README.md | 72 +++++++++++++++++++++++++++++- docker-compose.yml | 8 ++++ jsconfig.json | 27 ++++++++++++ package.json | 19 ++++++++ server.js | 108 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 server.js diff --git a/.gitignore b/.gitignore index ceaea36..2e917f1 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,7 @@ dist .yarn/install-state.gz .pnp.* +# extra +.idea/ +bun.lockb + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b551492 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM oven/bun:latest +WORKDIR /app +COPY package*.json /app/ +RUN bun install +COPY . /app +EXPOSE 3000 +CMD [ "bun", "run", "server.js" ] \ No newline at end of file diff --git a/README.md b/README.md index 8451b2f..1958de1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,73 @@ # mail-connect -API bridge for docker-mailserver \ No newline at end of file +API bridge for docker-mailserver + +*mail-connect is still in early beta* + +## What is it + +mail-connect aims to connect your `docker-mailserver` to *anything* you can imagine, through the power of an API. Despite being used as a core component of LibreCloud, you can still implement mail-connect any way you wish! + +We provide an extendable API which interacts with the `setup` utility via a Docker socket. While this offers advantages, mail-connect is still slow on some functions (such as listing accounts), as it's merely executing pre-made commands and parsing the output. + +## Features + +All features marked with an **E** are extended features, and are not a part of the original `setup` utility. + +### Email + +- [X] Create email +- [X] List emails +- [ ] **E** Create email from file +- [ ] Change password +- [ ] Delete email +- [ ] Restrict email + +### Alias + +- [ ] Create alias +- [ ] List aliases +- [ ] Delete alias + +### Quotas + +- [ ] Set quota +- [ ] Delete quota + +### dovecot-master + +- [ ] Add +- [ ] Update +- [ ] Delete +- [ ] List + +### Config + +- [ ] DKIM + +### Relay + +- [ ] Add auth +- [ ] Add domain +- [ ] Exclude domain + +### Fail2Ban + +- [ ] Ban IP +- [ ] Unban IP +- [ ] Ban log +- [ ] Fail2Ban status + +### Debug + +- [ ] Fetchmail +- [ ] Login +- [ ] Show mail logs + +## Future Improvements + +I plan to implement a *much* more powerful API, when everything else has settled. I will be taking a look at the setup utility itself, and seeing how a more efficient approach can be taken. + +Since `docker-mailserver` is built on Dovecot and Postfix, I am confident we can improve this API to be speedy and efficient as ever. + +## To-Do diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ac5dbc1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + mail-connect: + build: . + container_name: mail-connect + ports: + - "6723:3000" + volumes: + - '/var/run/docker.sock:/var/run/docker.sock' \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e445c4a --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "mail-connect", + "module": "server.js", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "child_process": "^1.0.2", + "dockerode": "^4.0.4", + "express": "^4.21.2" + }, + "trustedDependencies": [ + "protobufjs" + ] +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..bd7508c --- /dev/null +++ b/server.js @@ -0,0 +1,108 @@ +const express = require('express'); +const Docker = require('dockerode'); + +const docker = new Docker({ socketPath: '/var/run/docker.sock' }); +const app = express(); +app.use(express.json()); + +function listAccounts() { + return new Promise((resolve, reject) => { + const container = docker.getContainer('mailserver'); + container.exec({ + Cmd: ['setup', 'email', 'list'], + AttachStdout: true, + AttachStderr: true, + }, (err, exec) => { + if (err) { + return reject(err); + } + + exec.start((err, stream) => { + if (err) { + return reject(err); + } + + let output = ''; + stream.on('data', (chunk) => { + output += chunk.toString(); + }); + + stream.on('end', () => { + // Remove control characters + const cleanOutput = output.replace(/[\u0000-\u001F]+/g, ''); + const regex = /\*\s*(\S+)\s*\(\s*([^\s\/]+)\s*\/\s*([^)]+)\s*\)\s*\[(\d+)%]/g; + const accounts = []; + + for (const match of cleanOutput.matchAll(regex)) { + accounts.push({ + email: match[1], + used: match[2].trim() === '~' ? 'Unlimited' : match[2].trim(), + capacity: match[3].trim() === '~' ? 'Unlimited' : match[3].trim(), + percentage: match[4] + }); + } + + resolve(accounts); + }); + }); + }); + }); +} + +app.get('/list', (req, res) => { + listAccounts() + .then(accounts => res.json({ accounts })) + .catch(err => res.status(500).json({ error: err.message })); +}); + +app.post('/add', (req, res) => { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: "Email and password are required." }); + } + + const container = docker.getContainer('mailserver'); + container.exec({ + Cmd: ['setup', 'email', 'add', email, password], + AttachStdout: true, + AttachStderr: true, + }, (err, exec) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + exec.start((err, stream) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + let output = ''; + stream.on('data', (chunk) => { + output += chunk.toString(); + }); + + stream.on('end', async () => { + const cleanOutput = output.replace(/[\u0000-\u001F]+/g, '').trim(); + if (cleanOutput === '') { + return res.json({ success: true }); + } + + try { + const accounts = await listAccounts(); + const accountFound = accounts.find(acc => acc.email === email); + if (accountFound) { + return res.json({ success: true }); + } else { + return res.json({ success: false, message: "Account creation failed" }); + } + } catch (error) { + return res.status(500).json({ error: error.message }); + } + }); + }); + }); +}); + +app.listen(3000, () => { + console.log(`API listening on port 3000`); +}); \ No newline at end of file