Compare commits

...

44 Commits

Author SHA1 Message Date
bce1796115 Merge branch 'develop' 2021-05-28 14:51:31 +02:00
97c87a96f4 Version 2.4.2 2021-05-28 14:51:21 +02:00
e7d28000a4 Install and upgrade dependencies 2021-05-28 14:44:33 +02:00
87dbcf7e3c Merge branch 'boilerplate' into develop 2021-05-28 14:40:02 +02:00
679e8ab769 Make max public domain identity per user configurable, default unlimited 2021-05-28 14:38:15 +02:00
827e7644fa Merge branch 'develop' 2021-04-30 11:57:46 +02:00
efbc895ba3 Bump @types/node 2021-04-30 11:55:59 +02:00
2cb5b6f8f9 Replace node-sass with sass 2021-04-30 11:55:59 +02:00
f5d135d331 Merge branch 'develop' 2021-04-22 14:21:08 +02:00
82439366ba Fix imagemin peer dependency of img-loader 2021-04-22 14:06:53 +02:00
162f884103 Upgrade dependencies 2021-04-22 12:34:20 +02:00
00bb24cf2e Update repository url 2021-04-22 12:30:40 +02:00
911008af8b Merge branch 'develop' 2021-03-31 15:50:57 +02:00
6399573432 Version 2.4.1 2021-03-31 15:50:38 +02:00
14edb62823 Update to latest swaf changes on UserNameComponent 2021-03-31 15:43:08 +02:00
84081ccd8c Upgrade dependencies 2021-03-31 15:35:18 +02:00
2bf70ca9e1 Merge branch 'boilerplate' into develop 2021-03-31 15:34:27 +02:00
f90208c74e Fix user list not appearing in domains backend 2021-03-31 15:24:21 +02:00
4bcee79e8a Merge branch 'develop' 2021-03-30 16:56:44 +02:00
d223c9c77f Update config to new swaf version 2021-03-30 16:56:35 +02:00
faa728aacb layout.scss: allow .form-field to be hidden with .hidden 2021-03-30 14:36:06 +02:00
00b1f25804 Merge branch 'develop' 2021-03-30 12:45:12 +02:00
12f82f0f3d package.json: use NodeJS scripts instead of unix commands 2021-03-30 12:44:09 +02:00
ceaa0769bc Upgrade dependencies 2021-03-30 12:39:40 +02:00
0f83dd7ad6 layout.scss: add pagination default style 2021-03-30 12:34:24 +02:00
2bd1e9ac9f Merge branch 'develop' 2021-03-01 14:06:31 +01:00
3c702de71b Version 2.4.0 2021-03-01 14:06:03 +01:00
530f6c6043 Upgrade dependencies 2021-03-01 14:06:03 +01:00
b9949cdf5e Backend: separate mail domains from mailboxes 2021-03-01 14:01:02 +01:00
272e506281 MailboxBackend: add set main identity action 2021-03-01 13:43:29 +01:00
6478d77733 MailboxBackend: fix redirections 2021-03-01 13:29:30 +01:00
74f7d92437 mailbox backend: don't show identity delete action on mailbox identity 2021-02-26 12:57:40 +01:00
0a0d284ecc mailbox backend: fix create identity form title when mailbox exists 2021-02-26 12:57:14 +01:00
959e5ca276 Remove duplicate definition of .copyable-text and improve table layout 2021-01-26 14:30:46 +01:00
1cdd118806 Merge branch 'develop' 2021-01-26 13:37:58 +01:00
145d8df70a css: move menu .tip visibility to layout.scss 2021-01-26 13:37:41 +01:00
01a1216c99 css: move menu .tip visibility to layout.scss 2021-01-26 13:37:14 +01:00
2726fd16cc Merge branch 'develop' 2021-01-26 13:17:51 +01:00
8612b0f33a css: move lighter theme to layout.scss and make header css mobile first 2021-01-26 13:00:38 +01:00
bda464ab16 css: move a hover color and td.actions improvements to layout.scss 2021-01-26 13:00:23 +01:00
e7165edafd Add hover color on warning buttons 2021-01-26 11:43:42 +01:00
16394c0d4c Set icon size for titles 2021-01-26 11:43:13 +01:00
d2a67a12ee Fix subpanel leaking out of their container 2021-01-26 11:42:24 +01:00
1043690a5d Add bigger container max-width for desktop view 2021-01-26 11:38:26 +01:00
21 changed files with 2033 additions and 2565 deletions

View File

@ -88,6 +88,7 @@
},
"ignorePatterns": [
"jest.config.js",
"scripts/**/*",
"webpack.config.js",
"dist/**/*",
"public/**/*",

View File

@ -30,4 +30,4 @@ $errorColor: desaturate($errorText, 50%);
// Responsivity
$mobileThreshold: 632px;
$desktopThreshold: 632px;
$desktopThreshold: 940px;

View File

@ -10,28 +10,4 @@ body > header {
font-size: 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;
}
}
}
}
}
}

View File

@ -156,10 +156,31 @@ body > header {
}
.tip {
position: static;
visibility: visible;
opacity: 1;
display: block;
height: auto;
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;
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-left: 8px;
a, button, .button {
@include tip;
}
&:last-child {
a, button, .button {
.tip {
@ -390,7 +407,7 @@ form {
padding: 8px 16px;
text-align: center;
.form-field {
.form-field:not(.hidden) {
display: flex;
flex-direction: column;
margin: 16px auto;
@ -685,6 +702,7 @@ td.actions {
th {
border-bottom: 1px solid #39434a;
white-space: nowrap;
}
tr:nth-child(even) {
@ -851,37 +869,7 @@ td.actions {
}
.content {
overflow: hidden;
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 {
width: 0;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
@ -931,3 +919,34 @@ td.actions {
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;
}
}
}
}
}

View File

@ -1,69 +1,75 @@
{
app: {
name: 'Rainbox Email',
contact_email: 'contact@rainbox.email',
webmail_url: 'https://rc.rainbox.email',
},
log_level: "DEV",
db_log_level: "ERROR",
public_url: "http://localhost:4899",
public_websocket_url: "ws://localhost:4899",
port: 4899,
mysql: {
connectionLimit: 10,
host: "localhost",
user: "root",
password: "",
database: "rainbox_email",
create_database_automatically: false,
},
redis: {
host: "127.0.0.1",
port: 6379,
prefix: 'rainbox.email',
},
session: {
cookie: {
secure: false,
},
},
mail: {
host: "127.0.0.1",
port: "1025",
secure: false,
username: "",
password: "",
allow_invalid_tls: true,
from: 'contact@rainbox.email',
from_name: 'Rainbox Email',
},
view: {
cache: false,
},
approval_mode: false,
magic_link: {
validity_period: 20,
},
mail_autoconfig: {
main_domain: 'localhost',
display_name: 'Rainbox Email',
display_name_short: 'Rainbox',
username: '%EMAILADDRESS%',
auth_method: 'password-cleartext',
imap: {
server: 'localhost',
port: '143',
method: 'STARTTLS',
},
pop3: {
server: 'localhost',
port: '110',
method: 'STARTTLS',
},
smtp: {
server: 'localhost',
port: '587',
method: 'STARTTLS',
},
},
app: {
name: 'Rainbox Email',
contact_email: 'contact@rainbox.email',
webmail_url: 'https://rc.rainbox.email',
},
log_level: "DEV",
db_log_level: "ERROR",
public_url: "http://localhost:4899",
public_websocket_url: "ws://localhost:4899",
port: 4899,
mysql: {
connectionLimit: 10,
host: "localhost",
user: "root",
password: "",
database: "rainbox_email",
create_database_automatically: false,
},
redis: {
host: "127.0.0.1",
port: 6379,
prefix: 'rainbox.email',
},
session: {
cookie: {
secure: false,
},
},
mail: {
host: "127.0.0.1",
port: "1025",
secure: false,
username: "",
password: "",
allow_invalid_tls: true,
from: 'contact@rainbox.email',
from_name: 'Rainbox Email',
// Unlimited
max_public_identities_per_user: -1,
},
view: {
cache: false,
},
auth: {
approval_mode: false,
// 30 days
name_change_wait_period: 2592000000,
},
magic_link: {
validity_period: 20,
},
mail_autoconfig: {
main_domain: 'localhost',
display_name: 'Rainbox Email',
display_name_short: 'Rainbox',
username: '%EMAILADDRESS%',
auth_method: 'password-cleartext',
imap: {
server: 'localhost',
port: '143',
method: 'STARTTLS',
},
pop3: {
server: 'localhost',
port: '110',
method: 'STARTTLS',
},
smtp: {
server: 'localhost',
port: '587',
method: 'STARTTLS',
},
},
}

View File

@ -1,41 +1,43 @@
{
log_level: "DEBUG",
db_log_level: "ERROR",
public_url: "https://rainbox.email",
public_websocket_url: "wss://rainbox.email",
session: {
cookie: {
secure: true
}
},
mail: {
secure: true,
allow_invalid_tls: false
},
approval_mode: true,
magic_link: {
validity_period: 900,
},
mail_autoconfig: {
main_domain: 'rainbox.email',
display_name: 'Rainbox Email',
display_name_short: 'Rainbox',
username: '%EMAILADDRESS%',
auth_method: 'password-cleartext',
imap: {
server: 'rainbox.email',
port: '143',
method: 'STARTTLS',
},
pop3: {
server: 'rainbox.email',
port: '110',
method: 'STARTTLS',
},
smtp: {
server: 'rainbox.email',
port: '587',
method: 'STARTTLS',
},
},
log_level: "DEBUG",
db_log_level: "ERROR",
public_url: "https://rainbox.email",
public_websocket_url: "wss://rainbox.email",
session: {
cookie: {
secure: true,
},
},
mail: {
secure: true,
allow_invalid_tls: false,
},
auth: {
approval_mode: true,
},
magic_link: {
validity_period: 900,
},
mail_autoconfig: {
main_domain: 'rainbox.email',
display_name: 'Rainbox Email',
display_name_short: 'Rainbox',
username: '%EMAILADDRESS%',
auth_method: 'password-cleartext',
imap: {
server: 'rainbox.email',
port: '143',
method: 'STARTTLS',
},
pop3: {
server: 'rainbox.email',
port: '110',
method: 'STARTTLS',
},
smtp: {
server: 'rainbox.email',
port: '587',
method: 'STARTTLS',
},
},
}

View File

@ -6,4 +6,7 @@
database: "rainbox_email_test",
create_database_automatically: true
},
mail: {
max_public_identities_per_user: 1,
},
}

View File

@ -1,26 +1,25 @@
{
"name": "rainbox.email",
"version": "2.3.0",
"version": "2.4.2",
"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>",
"main": "dist/main.js",
"license": "AGPL-3.0-only",
"scripts": {
"test": "jest --verbose --runInBand",
"clean": "(test ! -d dist || rm -r dist)",
"prepareSources": "cp package.json src/",
"clean": "node scripts/clean.js",
"prepare-sources": "node scripts/prepare-sources.js",
"compile": "yarn clean && tsc",
"build": "yarn prepareSources && 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\"",
"build": "yarn prepare-sources && yarn compile && webpack --mode production",
"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",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
"lint": "eslint ."
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@fortawesome/fontawesome-free": "^5.14.0",
"@types/argon2": "^0.15.0",
"@types/config": "^0.0.38",
"@types/express": "^4.17.6",
"@types/express-session": "^1.17.0",
@ -29,33 +28,33 @@
"@types/jest": "^26.0.4",
"@types/ldapjs": "^1.0.7",
"@types/mysql": "^2.15.15",
"@types/node": "^14.6.3",
"@types/node": "^15.0.1",
"@types/nodemailer": "^6.4.0",
"@types/nunjucks": "^3.1.3",
"@types/ws": "^7.2.6",
"@typescript-eslint/eslint-plugin": "^4.3.0",
"@typescript-eslint/parser": "^4.3.0",
"babel-loader": "^8.1.0",
"concurrently": "^5.1.0",
"concurrently": "^6.0.0",
"css-loader": "^5.0.0",
"eslint": "^7.10.0",
"feather-icons": "^4.28.0",
"file-loader": "^6.0.0",
"imagemin": "^7.0.1",
"imagemin": "^7.0.0",
"imagemin-gifsicle": "^7.0.0",
"imagemin-mozjpeg": "^9.0.0",
"imagemin-pngquant": "^9.0.0",
"imagemin-svgo": "^8.0.0",
"imagemin-svgo": "^9.0.0",
"img-loader": "^3.0.1",
"jest": "^26.1.0",
"maildev": "^1.1.0",
"mini-css-extract-plugin": "^1.2.1",
"node-sass": "^5.0.0",
"nodemon": "^2.0.3",
"sass-loader": "^10.0.1",
"sass": "^1.32.12",
"sass-loader": "^11.0.1",
"terser-webpack-plugin": "^5.0.3",
"ts-jest": "^26.1.1",
"ts-loader": "^8.0.4",
"ts-loader": "^9.1.0",
"typescript": "^4.0.2",
"webpack": "^5.3.2",
"webpack-cli": "^4.1.0"

10
scripts/clean.js Normal file
View 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});
}
});

View File

@ -0,0 +1,4 @@
const fs = require('fs');
const path = require('path');
fs.copyFileSync('package.json', path.join('src', 'package.json'));

View File

@ -42,6 +42,7 @@ import AccountController from "swaf/auth/AccountController";
import packageJson = require('./package.json');
import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
import AddNameChangedAtToUsersMigration from "swaf/auth/migrations/AddNameChangedAtToUsersMigration";
export default class App extends Application {
public constructor(
@ -66,6 +67,7 @@ export default class App extends Application {
DropLegacyLogsTable,
AddUsedToMagicLinksMigration,
MakeMagicLinksSessionNotUniqueMigration,
AddNameChangedAtToUsersMigration,
];
}

View File

@ -57,21 +57,20 @@ export default class AccountMailboxController extends Controller {
});
// Check whether this identity can be created by this user
if (!domain.canCreateAddresses(user)) {
throw new ForbiddenHttpError('domain', req.url);
}
if (domain.isPublic()) {
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);
const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount();
const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount();
if (actualPublicAddressesCount >= maxPublicAddressesCount) {
if (maxPublicAddressesCount >= 0 && actualPublicAddressesCount >= maxPublicAddressesCount) {
req.flash('error', 'You have reached maximum public email addresses.');
res.redirect(Controller.route('account-mailbox'));
return;
}
} else {
if (!domain.canCreateAddresses(user)) {
throw new ForbiddenHttpError('domain', req.url);
}
}
// Save identity

View File

@ -59,7 +59,7 @@ export default class AccountBackendController extends Controller {
await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password');
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'));
}
}

View File

@ -3,7 +3,7 @@ import {Request, Response} from "express";
import User from "swaf/auth/models/User";
import {WhereTest} from "swaf/db/ModelQuery";
import UserMailIdentityComponent from "../../models/UserMailIdentityComponent";
import {NotFoundHttpError} from "swaf/HttpError";
import {NotFoundHttpError, ServerError} from "swaf/HttpError";
import MailDomain from "../../models/MailDomain";
import BackendController from "swaf/helpers/BackendController";
import MailIdentity from "../../models/MailIdentity";
@ -13,6 +13,11 @@ import UserNameComponent from "swaf/auth/models/UserNameComponent";
export default class MailboxBackendController extends Controller {
public constructor() {
super();
BackendController.registerMenuElement({
getLink: async () => Controller.route('backend-mail-domains'),
getDisplayString: async () => 'Mail domains',
getDisplayIcon: async () => 'globe',
});
BackendController.registerMenuElement({
getLink: async () => Controller.route('backend-mailboxes'),
getDisplayString: async () => 'Mailboxes',
@ -26,23 +31,20 @@ export default class MailboxBackendController extends Controller {
public routes(): void {
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.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('/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('/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);
}
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
const mailDomains = await MailDomain.select()
.with('owner')
.with('identities')
.get();
const users = await User.select()
.where('main_mail_identity_id', null, WhereTest.NE)
.with('mainMailIdentity')
@ -50,22 +52,9 @@ export default class MailboxBackendController extends Controller {
.get();
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 => ({
id: user.id,
username: user.as(UserNameComponent).name,
username: user.as(UserNameComponent).getName(),
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail(),
identity_count: (await user.as(UserMailIdentityComponent).mailIdentities.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', {
mailbox: {
id: user.id,
userName: user.as(UserNameComponent).name,
userName: user.as(UserNameComponent).getName(),
name: await mainMailIdentity?.toEmail() || 'Not created.',
exists: !!mainMailIdentity,
},
domains: mailDomains.map(d => ({
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> {
const domain = MailDomain.create(req.body);
await domain.save();
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> {
@ -133,7 +146,7 @@ export default class MailboxBackendController extends Controller {
await domain.save();
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> {
@ -146,7 +159,7 @@ export default class MailboxBackendController extends Controller {
// Don't delete that domain if it still has identities
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).`);
res.redirect(Controller.route('backend-mailboxes'));
res.redirect(Controller.route('backend-edit-domain', domain.id));
return;
}
@ -184,7 +197,7 @@ export default class MailboxBackendController extends Controller {
}
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> {
@ -194,15 +207,38 @@ export default class MailboxBackendController extends Controller {
.first();
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
const user = await identity.user.get();
if (user?.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
const user = identity.user.getOrFail();
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.`);
res.redirect(Controller.route('backend-mailbox', req.params.id));
res.redirect(Controller.route('backend-mailbox', user.id));
return;
}
await identity.delete();
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));
}
}

View File

@ -2,6 +2,7 @@ import Model from "swaf/db/Model";
import User from "swaf/auth/models/User";
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
import MailIdentity from "./MailIdentity";
import UserNameComponent from "swaf/auth/models/UserNameComponent";
export default class MailDomain extends Model {
public id?: number = undefined;
@ -38,6 +39,6 @@ export default class MailDomain extends Model {
}
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();
}
}

View File

@ -3,6 +3,7 @@ import User from "swaf/auth/models/User";
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
import MailIdentity from "./MailIdentity";
import MailDomain from "./MailDomain";
import config from "config";
export default class UserMailIdentityComponent extends ModelComponent<User> {
public main_mail_identity_id?: number = undefined;
@ -27,7 +28,7 @@ export default class UserMailIdentityComponent extends ModelComponent<User> {
});
public getMaxPublicAddressesCount(): number {
return 1;
return config.get<number>('mail.max_public_identities_per_user');
}
public async getPublicAddressesCount(): Promise<number> {

View File

@ -4,9 +4,9 @@
{% block body %}
<div class="container">
{{ macros.breadcrumb('Mail domain: ' + domain.name, [
{{ macros.breadcrumb(domain.name, [
{title: 'Backend', link: route('backend')},
{title: 'Mailboxes', link: route('backend-mailboxes')}
{title: 'Mail domains', link: route('backend-mail-domains')}
]) }}
<div class="panel">

View 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 %}

View File

@ -28,16 +28,29 @@
<td>{{ identity.id }}</td>
<td>{{ identity.email }}</td>
<td class="actions">
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
<input type="hidden" name="id" value="{{ identity.id }}">
{% 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="danger"
onclick="return confirm('Are you sure you want to delete {{ identity.email }}?')">
<i data-feather="trash"></i> <span class="tip">Delete</span>
</button>
<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>
{{ macros.csrf(getCsrfToken) }}
</form>
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
<input type="hidden" name="id" value="{{ identity.id }}">
<button class="danger"
onclick="return confirm('Are you sure you want to delete {{ identity.email }}?')">
<i data-feather="trash"></i> <span class="tip">Delete</span>
</button>
{{ macros.csrf(getCsrfToken) }}
</form>
{% endif %}
</td>
</tr>
{% endfor %}
@ -45,7 +58,7 @@
</table>
<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">
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}

View File

@ -10,60 +10,6 @@
<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">
<h2><i data-feather="mail"></i> Mailboxes</h2>

3939
yarn.lock

File diff suppressed because it is too large Load Diff