Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
bce1796115 | |||
97c87a96f4 | |||
e7d28000a4 | |||
87dbcf7e3c | |||
679e8ab769 | |||
827e7644fa | |||
efbc895ba3 | |||
2cb5b6f8f9 | |||
f5d135d331 | |||
82439366ba | |||
162f884103 | |||
00bb24cf2e | |||
911008af8b | |||
6399573432 | |||
14edb62823 | |||
84081ccd8c | |||
2bf70ca9e1 | |||
f90208c74e | |||
4bcee79e8a | |||
d223c9c77f | |||
faa728aacb | |||
00b1f25804 | |||
12f82f0f3d | |||
ceaa0769bc | |||
0f83dd7ad6 | |||
2bd1e9ac9f | |||
3c702de71b | |||
530f6c6043 | |||
b9949cdf5e | |||
272e506281 | |||
6478d77733 | |||
74f7d92437 | |||
0a0d284ecc | |||
959e5ca276 | |||
1cdd118806 | |||
145d8df70a | |||
01a1216c99 | |||
2726fd16cc | |||
8612b0f33a | |||
bda464ab16 | |||
e7165edafd | |||
16394c0d4c | |||
d2a67a12ee | |||
1043690a5d |
@ -88,6 +88,7 @@
|
|||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"jest.config.js",
|
"jest.config.js",
|
||||||
|
"scripts/**/*",
|
||||||
"webpack.config.js",
|
"webpack.config.js",
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
|
@ -30,4 +30,4 @@ $errorColor: desaturate($errorText, 50%);
|
|||||||
|
|
||||||
// Responsivity
|
// Responsivity
|
||||||
$mobileThreshold: 632px;
|
$mobileThreshold: 632px;
|
||||||
$desktopThreshold: 632px;
|
$desktopThreshold: 940px;
|
||||||
|
@ -10,28 +10,4 @@ body > header {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
|
||||||
ul {
|
|
||||||
li {
|
|
||||||
a, button {
|
|
||||||
.tip {
|
|
||||||
position: static;
|
|
||||||
display: block;
|
|
||||||
visibility: visible;
|
|
||||||
height: auto;
|
|
||||||
padding: 0 0 0 4px;
|
|
||||||
transform: none;
|
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
background: transparent;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -156,10 +156,31 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tip {
|
.tip {
|
||||||
|
position: static;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
display: block;
|
display: block;
|
||||||
|
height: auto;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
text-transform: inherit;
|
padding: 0 0 0 4px;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
color: inherit;
|
||||||
|
text-transform: uppercase;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.tip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-out 100ms;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,10 +272,6 @@ body > header {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
|
||||||
a, button, .button {
|
|
||||||
@include tip;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
a, button, .button {
|
a, button, .button {
|
||||||
.tip {
|
.tip {
|
||||||
@ -390,7 +407,7 @@ form {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.form-field {
|
.form-field:not(.hidden) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 16px auto;
|
margin: 16px auto;
|
||||||
@ -685,6 +702,7 @@ td.actions {
|
|||||||
|
|
||||||
th {
|
th {
|
||||||
border-bottom: 1px solid #39434a;
|
border-bottom: 1px solid #39434a;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
@ -851,37 +869,7 @@ td.actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
overflow: hidden;
|
width: 0;
|
||||||
white-space: nowrap;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 20px;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyable-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 8px;
|
|
||||||
|
|
||||||
background-color: darken($backgroundColor, 2%);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -931,3 +919,34 @@ td.actions {
|
|||||||
background: $secondary;
|
background: $secondary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-col-grow {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
list-style: none;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
li {
|
||||||
|
a, &.active, &.ellipsis {
|
||||||
|
display: block;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
line-height: 32px;
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
&:hover:not(.active):not(.ellipsis) {
|
||||||
|
background-color: #fff5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -36,11 +36,17 @@
|
|||||||
allow_invalid_tls: true,
|
allow_invalid_tls: true,
|
||||||
from: 'contact@rainbox.email',
|
from: 'contact@rainbox.email',
|
||||||
from_name: 'Rainbox Email',
|
from_name: 'Rainbox Email',
|
||||||
|
// Unlimited
|
||||||
|
max_public_identities_per_user: -1,
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
cache: false,
|
cache: false,
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
approval_mode: false,
|
approval_mode: false,
|
||||||
|
// 30 days
|
||||||
|
name_change_wait_period: 2592000000,
|
||||||
|
},
|
||||||
magic_link: {
|
magic_link: {
|
||||||
validity_period: 20,
|
validity_period: 20,
|
||||||
},
|
},
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
public_websocket_url: "wss://rainbox.email",
|
public_websocket_url: "wss://rainbox.email",
|
||||||
session: {
|
session: {
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: true
|
secure: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
secure: true,
|
secure: true,
|
||||||
allow_invalid_tls: false
|
allow_invalid_tls: false,
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
approval_mode: true,
|
approval_mode: true,
|
||||||
|
},
|
||||||
magic_link: {
|
magic_link: {
|
||||||
validity_period: 900,
|
validity_period: 900,
|
||||||
},
|
},
|
||||||
|
@ -6,4 +6,7 @@
|
|||||||
database: "rainbox_email_test",
|
database: "rainbox_email_test",
|
||||||
create_database_automatically: true
|
create_database_automatically: true
|
||||||
},
|
},
|
||||||
|
mail: {
|
||||||
|
max_public_identities_per_user: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
29
package.json
29
package.json
@ -1,26 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "rainbox.email",
|
"name": "rainbox.email",
|
||||||
"version": "2.3.0",
|
"version": "2.4.2",
|
||||||
"description": "ISP mail provider manager with mysql and integrated LDAP server",
|
"description": "ISP mail provider manager with mysql and integrated LDAP server",
|
||||||
"repository": "https://gitlab.com/ArisuOngaku/rainbox.email",
|
"repository": "https://eternae.ink/ashpie/rainbox.email",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --verbose --runInBand",
|
"test": "jest --verbose --runInBand",
|
||||||
"clean": "(test ! -d dist || rm -r dist)",
|
"clean": "node scripts/clean.js",
|
||||||
"prepareSources": "cp package.json src/",
|
"prepare-sources": "node scripts/prepare-sources.js",
|
||||||
"compile": "yarn clean && tsc",
|
"compile": "yarn clean && tsc",
|
||||||
"build": "yarn prepareSources && yarn compile && webpack --mode production",
|
"build": "yarn prepare-sources && yarn compile && webpack --mode production",
|
||||||
"dev": "yarn prepareSources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
||||||
"start": "yarn build && node",
|
"start": "yarn build && node",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.5",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@types/argon2": "^0.15.0",
|
|
||||||
"@types/config": "^0.0.38",
|
"@types/config": "^0.0.38",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
@ -29,33 +28,33 @@
|
|||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^26.0.4",
|
||||||
"@types/ldapjs": "^1.0.7",
|
"@types/ldapjs": "^1.0.7",
|
||||||
"@types/mysql": "^2.15.15",
|
"@types/mysql": "^2.15.15",
|
||||||
"@types/node": "^14.6.3",
|
"@types/node": "^15.0.1",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/nunjucks": "^3.1.3",
|
"@types/nunjucks": "^3.1.3",
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
||||||
"@typescript-eslint/parser": "^4.3.0",
|
"@typescript-eslint/parser": "^4.3.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"concurrently": "^5.1.0",
|
"concurrently": "^6.0.0",
|
||||||
"css-loader": "^5.0.0",
|
"css-loader": "^5.0.0",
|
||||||
"eslint": "^7.10.0",
|
"eslint": "^7.10.0",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"imagemin": "^7.0.1",
|
"imagemin": "^7.0.0",
|
||||||
"imagemin-gifsicle": "^7.0.0",
|
"imagemin-gifsicle": "^7.0.0",
|
||||||
"imagemin-mozjpeg": "^9.0.0",
|
"imagemin-mozjpeg": "^9.0.0",
|
||||||
"imagemin-pngquant": "^9.0.0",
|
"imagemin-pngquant": "^9.0.0",
|
||||||
"imagemin-svgo": "^8.0.0",
|
"imagemin-svgo": "^9.0.0",
|
||||||
"img-loader": "^3.0.1",
|
"img-loader": "^3.0.1",
|
||||||
"jest": "^26.1.0",
|
"jest": "^26.1.0",
|
||||||
"maildev": "^1.1.0",
|
"maildev": "^1.1.0",
|
||||||
"mini-css-extract-plugin": "^1.2.1",
|
"mini-css-extract-plugin": "^1.2.1",
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.3",
|
||||||
"sass-loader": "^10.0.1",
|
"sass": "^1.32.12",
|
||||||
|
"sass-loader": "^11.0.1",
|
||||||
"terser-webpack-plugin": "^5.0.3",
|
"terser-webpack-plugin": "^5.0.3",
|
||||||
"ts-jest": "^26.1.1",
|
"ts-jest": "^26.1.1",
|
||||||
"ts-loader": "^8.0.4",
|
"ts-loader": "^9.1.0",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.0.2",
|
||||||
"webpack": "^5.3.2",
|
"webpack": "^5.3.2",
|
||||||
"webpack-cli": "^4.1.0"
|
"webpack-cli": "^4.1.0"
|
||||||
|
10
scripts/clean.js
Normal file
10
scripts/clean.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
[
|
||||||
|
'dist',
|
||||||
|
].forEach(file => {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
console.log('Cleaning', file, '...');
|
||||||
|
fs.rmSync(file, {recursive: true});
|
||||||
|
}
|
||||||
|
});
|
4
scripts/prepare-sources.js
Normal file
4
scripts/prepare-sources.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
fs.copyFileSync('package.json', path.join('src', 'package.json'));
|
@ -42,6 +42,7 @@ import AccountController from "swaf/auth/AccountController";
|
|||||||
import packageJson = require('./package.json');
|
import packageJson = require('./package.json');
|
||||||
import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
|
import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
|
||||||
import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
|
import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
|
||||||
|
import AddNameChangedAtToUsersMigration from "swaf/auth/migrations/AddNameChangedAtToUsersMigration";
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -66,6 +67,7 @@ export default class App extends Application {
|
|||||||
DropLegacyLogsTable,
|
DropLegacyLogsTable,
|
||||||
AddUsedToMagicLinksMigration,
|
AddUsedToMagicLinksMigration,
|
||||||
MakeMagicLinksSessionNotUniqueMigration,
|
MakeMagicLinksSessionNotUniqueMigration,
|
||||||
|
AddNameChangedAtToUsersMigration,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,21 +57,20 @@ export default class AccountMailboxController extends Controller {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check whether this identity can be created by this user
|
// Check whether this identity can be created by this user
|
||||||
|
if (!domain.canCreateAddresses(user)) {
|
||||||
|
throw new ForbiddenHttpError('domain', req.url);
|
||||||
|
}
|
||||||
if (domain.isPublic()) {
|
if (domain.isPublic()) {
|
||||||
await Validator.validate({
|
await Validator.validate({
|
||||||
name: new Validator<string>().defined().equals(user.as(UserNameComponent).name),
|
name: new Validator<string>().defined().equals(user.as(UserNameComponent).getName()),
|
||||||
}, req.body);
|
}, req.body);
|
||||||
const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount();
|
const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount();
|
||||||
const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount();
|
const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount();
|
||||||
if (actualPublicAddressesCount >= maxPublicAddressesCount) {
|
if (maxPublicAddressesCount >= 0 && actualPublicAddressesCount >= maxPublicAddressesCount) {
|
||||||
req.flash('error', 'You have reached maximum public email addresses.');
|
req.flash('error', 'You have reached maximum public email addresses.');
|
||||||
res.redirect(Controller.route('account-mailbox'));
|
res.redirect(Controller.route('account-mailbox'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (!domain.canCreateAddresses(user)) {
|
|
||||||
throw new ForbiddenHttpError('domain', req.url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save identity
|
// Save identity
|
||||||
|
@ -59,7 +59,7 @@ export default class AccountBackendController extends Controller {
|
|||||||
await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password');
|
await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password');
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
req.flash('success', `New password set for ${user.as(UserNameComponent).name}`);
|
req.flash('success', `New password set for ${user.as(UserNameComponent).getName()}`);
|
||||||
res.redirect(Controller.route('backend-list-users'));
|
res.redirect(Controller.route('backend-list-users'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import {Request, Response} from "express";
|
|||||||
import User from "swaf/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {WhereTest} from "swaf/db/ModelQuery";
|
import {WhereTest} from "swaf/db/ModelQuery";
|
||||||
import UserMailIdentityComponent from "../../models/UserMailIdentityComponent";
|
import UserMailIdentityComponent from "../../models/UserMailIdentityComponent";
|
||||||
import {NotFoundHttpError} from "swaf/HttpError";
|
import {NotFoundHttpError, ServerError} from "swaf/HttpError";
|
||||||
import MailDomain from "../../models/MailDomain";
|
import MailDomain from "../../models/MailDomain";
|
||||||
import BackendController from "swaf/helpers/BackendController";
|
import BackendController from "swaf/helpers/BackendController";
|
||||||
import MailIdentity from "../../models/MailIdentity";
|
import MailIdentity from "../../models/MailIdentity";
|
||||||
@ -13,6 +13,11 @@ import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
|||||||
export default class MailboxBackendController extends Controller {
|
export default class MailboxBackendController extends Controller {
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
|
BackendController.registerMenuElement({
|
||||||
|
getLink: async () => Controller.route('backend-mail-domains'),
|
||||||
|
getDisplayString: async () => 'Mail domains',
|
||||||
|
getDisplayIcon: async () => 'globe',
|
||||||
|
});
|
||||||
BackendController.registerMenuElement({
|
BackendController.registerMenuElement({
|
||||||
getLink: async () => Controller.route('backend-mailboxes'),
|
getLink: async () => Controller.route('backend-mailboxes'),
|
||||||
getDisplayString: async () => 'Mailboxes',
|
getDisplayString: async () => 'Mailboxes',
|
||||||
@ -26,23 +31,20 @@ export default class MailboxBackendController extends Controller {
|
|||||||
|
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
this.get('/', this.getMailboxesBackend, 'backend-mailboxes', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.get('/', this.getMailboxesBackend, 'backend-mailboxes', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.get('/:id', this.getMailboxBackend, 'backend-mailbox', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.get('/mailbox/:id', this.getMailboxBackend, 'backend-mailbox', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
|
|
||||||
|
this.get('/domains', this.getDomainsBackend, 'backend-mail-domains', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.post('/add-domain', this.postAddDomain, 'backend-add-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.post('/add-domain', this.postAddDomain, 'backend-add-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.get('/edit-domain/:id', this.getEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.get('/edit-domain/:id', this.getEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.post('/edit-domain/:id', this.postEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.post('/edit-domain/:id', this.postEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.post('/remove-domain', this.postRemoveDomain, 'backend-remove-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.post('/remove-domain', this.postRemoveDomain, 'backend-remove-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
|
|
||||||
this.post('/:id/create-mail-identity', this.postCreateMailIdentity, 'backend-create-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.post('/:id/create-mail-identity', this.postCreateMailIdentity, 'backend-create-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
|
this.post('/set-main-mail-identity', this.postSetMainMailIdentity, 'backend-set-main-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'backend-delete-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'backend-delete-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
|
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
|
||||||
const mailDomains = await MailDomain.select()
|
|
||||||
.with('owner')
|
|
||||||
.with('identities')
|
|
||||||
.get();
|
|
||||||
|
|
||||||
const users = await User.select()
|
const users = await User.select()
|
||||||
.where('main_mail_identity_id', null, WhereTest.NE)
|
.where('main_mail_identity_id', null, WhereTest.NE)
|
||||||
.with('mainMailIdentity')
|
.with('mainMailIdentity')
|
||||||
@ -50,22 +52,9 @@ export default class MailboxBackendController extends Controller {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
res.render('backend/mailboxes', {
|
res.render('backend/mailboxes', {
|
||||||
domains: await Promise.all(mailDomains.map(async domain => ({
|
|
||||||
id: domain.id,
|
|
||||||
name: domain.name,
|
|
||||||
owner_name: (await domain.owner.get())?.as(UserNameComponent).name,
|
|
||||||
identity_count: (await domain.identities.get()).length,
|
|
||||||
}))),
|
|
||||||
users: [{
|
|
||||||
value: 0,
|
|
||||||
display: 'Public',
|
|
||||||
}, ...(await User.select().get()).map(u => ({
|
|
||||||
value: u.id,
|
|
||||||
display: u.name,
|
|
||||||
}))],
|
|
||||||
mailboxes: await Promise.all(users.map(async user => ({
|
mailboxes: await Promise.all(users.map(async user => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.as(UserNameComponent).name,
|
username: user.as(UserNameComponent).getName(),
|
||||||
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail(),
|
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail(),
|
||||||
identity_count: (await user.as(UserMailIdentityComponent).mailIdentities.get()).length,
|
identity_count: (await user.as(UserMailIdentityComponent).mailIdentities.get()).length,
|
||||||
domain_count: (await user.as(UserMailIdentityComponent).mailDomains.get()).length,
|
domain_count: (await user.as(UserMailIdentityComponent).mailDomains.get()).length,
|
||||||
@ -86,8 +75,9 @@ export default class MailboxBackendController extends Controller {
|
|||||||
res.render('backend/mailbox', {
|
res.render('backend/mailbox', {
|
||||||
mailbox: {
|
mailbox: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
userName: user.as(UserNameComponent).name,
|
userName: user.as(UserNameComponent).getName(),
|
||||||
name: await mainMailIdentity?.toEmail() || 'Not created.',
|
name: await mainMailIdentity?.toEmail() || 'Not created.',
|
||||||
|
exists: !!mainMailIdentity,
|
||||||
},
|
},
|
||||||
domains: mailDomains.map(d => ({
|
domains: mailDomains.map(d => ({
|
||||||
display: d.name,
|
display: d.name,
|
||||||
@ -100,11 +90,34 @@ export default class MailboxBackendController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getDomainsBackend(req: Request, res: Response): Promise<void> {
|
||||||
|
const mailDomains = await MailDomain.select()
|
||||||
|
.with('owner')
|
||||||
|
.with('identities')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
res.render('backend/mail_domains', {
|
||||||
|
domains: await Promise.all(mailDomains.map(async domain => ({
|
||||||
|
id: domain.id,
|
||||||
|
name: domain.name,
|
||||||
|
owner_name: (await domain.owner.get())?.as(UserNameComponent).getName(),
|
||||||
|
identity_count: (await domain.identities.get()).length,
|
||||||
|
}))),
|
||||||
|
users: [{
|
||||||
|
value: 0,
|
||||||
|
display: 'Public',
|
||||||
|
}, ...(await User.select().get()).map(u => ({
|
||||||
|
value: u.id,
|
||||||
|
display: u.name,
|
||||||
|
}))],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected async postAddDomain(req: Request, res: Response): Promise<void> {
|
protected async postAddDomain(req: Request, res: Response): Promise<void> {
|
||||||
const domain = MailDomain.create(req.body);
|
const domain = MailDomain.create(req.body);
|
||||||
await domain.save();
|
await domain.save();
|
||||||
req.flash('success', `Domain ${domain.name} successfully added with owner ${(await domain.owner.get())?.name}`);
|
req.flash('success', `Domain ${domain.name} successfully added with owner ${(await domain.owner.get())?.name}`);
|
||||||
res.redirect(Controller.route('backend-mailboxes'));
|
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getEditDomain(req: Request, res: Response): Promise<void> {
|
protected async getEditDomain(req: Request, res: Response): Promise<void> {
|
||||||
@ -133,7 +146,7 @@ export default class MailboxBackendController extends Controller {
|
|||||||
await domain.save();
|
await domain.save();
|
||||||
|
|
||||||
req.flash('success', `Domain ${domain.name} updated successfully.`);
|
req.flash('success', `Domain ${domain.name} updated successfully.`);
|
||||||
res.redirect(Controller.route('backend-mailboxes'));
|
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postRemoveDomain(req: Request, res: Response): Promise<void> {
|
protected async postRemoveDomain(req: Request, res: Response): Promise<void> {
|
||||||
@ -146,7 +159,7 @@ export default class MailboxBackendController extends Controller {
|
|||||||
// Don't delete that domain if it still has identities
|
// Don't delete that domain if it still has identities
|
||||||
if ((await domain.identities.get()).length > 0) {
|
if ((await domain.identities.get()).length > 0) {
|
||||||
req.flash('error', `This domain still has identities. Please remove all of these first (don't forget to rename mailboxes).`);
|
req.flash('error', `This domain still has identities. Please remove all of these first (don't forget to rename mailboxes).`);
|
||||||
res.redirect(Controller.route('backend-mailboxes'));
|
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +197,7 @@ export default class MailboxBackendController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
||||||
res.redirect(Controller.route('backend-mailbox', req.params.id));
|
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
||||||
@ -194,15 +207,38 @@ export default class MailboxBackendController extends Controller {
|
|||||||
.first();
|
.first();
|
||||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||||
|
|
||||||
const user = await identity.user.get();
|
const user = identity.user.getOrFail();
|
||||||
if (user?.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
if (!user) throw new NotFoundHttpError('Mail identity owner', req.url);
|
||||||
|
if (user.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
||||||
req.flash('error', `Cannot delete this user's mailbox identity.`);
|
req.flash('error', `Cannot delete this user's mailbox identity.`);
|
||||||
res.redirect(Controller.route('backend-mailbox', req.params.id));
|
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await identity.delete();
|
await identity.delete();
|
||||||
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
||||||
res.redirect(Controller.route('backend-mailbox', req.params.id));
|
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postSetMainMailIdentity(req: Request, res: Response): Promise<void> {
|
||||||
|
const identity = await MailIdentity.select()
|
||||||
|
.where('id', req.body.id)
|
||||||
|
.with('user.mainMailIdentity')
|
||||||
|
.first();
|
||||||
|
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||||
|
|
||||||
|
const user = await identity.user.getOrFail();
|
||||||
|
if (!user) throw new NotFoundHttpError('Mail identity owner', req.url);
|
||||||
|
|
||||||
|
const mailIdentityComponent = user.as(UserMailIdentityComponent);
|
||||||
|
const mainMailIdentity = mailIdentityComponent.mainMailIdentity.getOrFail();
|
||||||
|
if (!mainMailIdentity) throw new ServerError('Could not find this users main mail identity.');
|
||||||
|
|
||||||
|
mailIdentityComponent.main_mail_identity_id = identity.id;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
req.flash('success', 'User ' + user.id + ' main mail identity set to ' + await identity.toEmail());
|
||||||
|
req.flash('warning', 'Please rename user\'s mailbox folder to correspond to changing from ' + await mainMailIdentity.toEmail() + ' to ' + await identity.toEmail());
|
||||||
|
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import Model from "swaf/db/Model";
|
|||||||
import User from "swaf/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
|
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
|
||||||
import MailIdentity from "./MailIdentity";
|
import MailIdentity from "./MailIdentity";
|
||||||
|
import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
||||||
|
|
||||||
export default class MailDomain extends Model {
|
export default class MailDomain extends Model {
|
||||||
public id?: number = undefined;
|
public id?: number = undefined;
|
||||||
@ -38,6 +39,6 @@ export default class MailDomain extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCreateAddresses(user: User): boolean {
|
public canCreateAddresses(user: User): boolean {
|
||||||
return this.user_id === user.id || this.isPublic();
|
return this.user_id === user.id || this.isPublic() && user.as(UserNameComponent).hasName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import User from "swaf/auth/models/User";
|
|||||||
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
|
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
|
||||||
import MailIdentity from "./MailIdentity";
|
import MailIdentity from "./MailIdentity";
|
||||||
import MailDomain from "./MailDomain";
|
import MailDomain from "./MailDomain";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
export default class UserMailIdentityComponent extends ModelComponent<User> {
|
export default class UserMailIdentityComponent extends ModelComponent<User> {
|
||||||
public main_mail_identity_id?: number = undefined;
|
public main_mail_identity_id?: number = undefined;
|
||||||
@ -27,7 +28,7 @@ export default class UserMailIdentityComponent extends ModelComponent<User> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
public getMaxPublicAddressesCount(): number {
|
public getMaxPublicAddressesCount(): number {
|
||||||
return 1;
|
return config.get<number>('mail.max_public_identities_per_user');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPublicAddressesCount(): Promise<number> {
|
public async getPublicAddressesCount(): Promise<number> {
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{ macros.breadcrumb('Mail domain: ' + domain.name, [
|
{{ macros.breadcrumb(domain.name, [
|
||||||
{title: 'Backend', link: route('backend')},
|
{title: 'Backend', link: route('backend')},
|
||||||
{title: 'Mailboxes', link: route('backend-mailboxes')}
|
{title: 'Mail domains', link: route('backend-mail-domains')}
|
||||||
]) }}
|
]) }}
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
67
views/backend/mail_domains.njk
Normal file
67
views/backend/mail_domains.njk
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - Backend' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
{{ macros.breadcrumb('Mail domains', [
|
||||||
|
{title: 'Backend', link: route('backend')}
|
||||||
|
]) }}
|
||||||
|
|
||||||
|
<h1>Domain manager</h1>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="globe"></i> Domains</h2>
|
||||||
|
|
||||||
|
<form action="{{ route('backend-add-domain') }}" method="POST" class="sub-panel">
|
||||||
|
<h3>Add domain</h3>
|
||||||
|
{{ macros.field(_locals, 'text', 'name', null, 'Domain name', null, 'required') }}
|
||||||
|
|
||||||
|
{{ macros.field(_locals, 'select', 'user_id', undefined, 'Owner', null, 'required', users) }}
|
||||||
|
|
||||||
|
<button><i data-feather="plus"></i> Add domain</button>
|
||||||
|
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Identities</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ domain.id }}</td>
|
||||||
|
<td>{{ domain.name }}</td>
|
||||||
|
<td>{{ domain.owner_name | default('Public') }}</td>
|
||||||
|
<td>{{ domain.identity_count }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<a href="{{ route('backend-edit-domain', domain.id) }}" class="button">
|
||||||
|
<i data-feather="edit-2"></i> <span class="tip">Edit</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form action="{{ route('backend-remove-domain') }}" method="POST">
|
||||||
|
<input type="hidden" name="id" value="{{ domain.id }}">
|
||||||
|
|
||||||
|
<button class="danger"
|
||||||
|
onclick="return confirm('Are you sure you want to delete {{ domain.name }}?')">
|
||||||
|
<i data-feather="trash"></i> <span class="tip">Remove</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -28,6 +28,18 @@
|
|||||||
<td>{{ identity.id }}</td>
|
<td>{{ identity.id }}</td>
|
||||||
<td>{{ identity.email }}</td>
|
<td>{{ identity.email }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
|
{% if mailbox.name != identity.email %}
|
||||||
|
<form action="{{ route('backend-set-main-mail-identity') }}" method="POST">
|
||||||
|
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||||
|
|
||||||
|
<button class=""
|
||||||
|
onclick="return confirm('Are you sure you want to set {{ identity.email }} as this mailbox\'s identity? This requires moving existing emails on the mail server afterwards.')">
|
||||||
|
<i data-feather="tag"></i> <span class="tip">Set as main identity</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
||||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||||
|
|
||||||
@ -38,6 +50,7 @@
|
|||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -45,7 +58,7 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<form action="{{ route('backend-create-mail-identity', mailbox.id) }}" method="POST" class="sub-panel">
|
<form action="{{ route('backend-create-mail-identity', mailbox.id) }}" method="POST" class="sub-panel">
|
||||||
<h3>{% if mailboxIdentity == null %}Create a mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
<h3>{% if not mailbox.exists %}Create a mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
||||||
|
|
||||||
<div class="inline-fields">
|
<div class="inline-fields">
|
||||||
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
||||||
|
@ -10,60 +10,6 @@
|
|||||||
|
|
||||||
<h1>Mailbox manager</h1>
|
<h1>Mailbox manager</h1>
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="globe"></i> Domains</h2>
|
|
||||||
|
|
||||||
<form action="{{ route('backend-add-domain') }}" method="POST" class="sub-panel">
|
|
||||||
<h3>Add domain</h3>
|
|
||||||
{{ macros.field(_locals, 'text', 'name', null, 'Domain name', null, 'required') }}
|
|
||||||
|
|
||||||
{{ macros.field(_locals, 'select', 'user_id', undefined, 'Owner', null, 'required', users) }}
|
|
||||||
|
|
||||||
<button><i data-feather="plus"></i> Add domain</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Owner</th>
|
|
||||||
<th>Identities</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for domain in domains %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ domain.id }}</td>
|
|
||||||
<td>{{ domain.name }}</td>
|
|
||||||
<td>{{ domain.owner_name | default('Public') }}</td>
|
|
||||||
<td>{{ domain.identity_count }}</td>
|
|
||||||
<td class="actions">
|
|
||||||
<a href="{{ route('backend-edit-domain', domain.id) }}" class="button">
|
|
||||||
<i data-feather="edit-2"></i> <span class="tip">Edit</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<form action="{{ route('backend-remove-domain') }}" method="POST">
|
|
||||||
<input type="hidden" name="id" value="{{ domain.id }}">
|
|
||||||
|
|
||||||
<button class="danger"
|
|
||||||
onclick="return confirm('Are you sure you want to delete {{ domain.name }}?')">
|
|
||||||
<i data-feather="trash"></i> <span class="tip">Remove</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2><i data-feather="mail"></i> Mailboxes</h2>
|
<h2><i data-feather="mail"></i> Mailboxes</h2>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user