create initial api (add, list endpoints) - NOT SECURE

This commit is contained in:
Aidan 2025-02-14 20:48:15 -05:00
parent 6f05cfc340
commit edc18c2c18
7 changed files with 244 additions and 1 deletions

4
.gitignore vendored
View File

@ -130,3 +130,7 @@ dist
.yarn/install-state.gz
.pnp.*
# extra
.idea/
bun.lockb

7
Dockerfile Normal file
View File

@ -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" ]

View File

@ -1,3 +1,73 @@
# mail-connect
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

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
services:
mail-connect:
build: .
container_name: mail-connect
ports:
- "6723:3000"
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'

27
jsconfig.json Normal file
View File

@ -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
}
}

19
package.json Normal file
View File

@ -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"
]
}

108
server.js Normal file
View File

@ -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`);
});