1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-02-01 13:17:55 +02:00

Removing sendMail compatibility.

Our bullseye also does not support it natively. Let's just use SMTP. That makes everything simpler.

 #683
This commit is contained in:
Patrik J. Braun 2023-08-04 00:24:43 +02:00
parent 1edb07dbf9
commit 891c155cce
8 changed files with 382 additions and 496 deletions

View File

@ -1,5 +0,0 @@
/**
* Keeps the environment context
* Only use it in the Config constructor
*/
export const ServerEnvironment: { sendMailAvailable?: boolean } = {};

View File

@ -26,16 +26,13 @@ import {
import {SearchQueryParser} from '../../../common/SearchQueryParser';
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
import {Utils} from '../../../common/Utils';
import {createTransport} from 'nodemailer';
import {EmailMessagingType, MessagingConfig} from '../../../common/config/private/MessagingConfig';
import {ServerEnvironment} from '../../Environment';
const LOG_TAG = '[ConfigDiagnostics]';
export class ConfigDiagnostics {
static testAlbumsConfig(
albumConfig: ClientAlbumConfig,
original: PrivateConfigClass
albumConfig: ClientAlbumConfig,
original: PrivateConfigClass
): void {
Logger.debug(LOG_TAG, 'Testing album config');
// nothing to check
@ -54,27 +51,27 @@ export class ConfigDiagnostics {
}
static async testDatabase(
databaseConfig: ServerDataBaseConfig
databaseConfig: ServerDataBaseConfig
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing database config');
await SQLConnection.tryConnection(databaseConfig);
if (databaseConfig.type === DatabaseType.sqlite) {
try {
await this.checkReadWritePermission(
SQLConnection.getSQLiteDB(databaseConfig)
SQLConnection.getSQLiteDB(databaseConfig)
);
} catch (e) {
throw new Error(
'Cannot read or write sqlite storage file: ' +
SQLConnection.getSQLiteDB(databaseConfig)
'Cannot read or write sqlite storage file: ' +
SQLConnection.getSQLiteDB(databaseConfig)
);
}
}
}
static async testMetaFileConfig(
metaFileConfig: ClientMetaFileConfig,
config: PrivateConfigClass
metaFileConfig: ClientMetaFileConfig,
config: PrivateConfigClass
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing meta file config');
if (metaFileConfig.gpx === true && config.Map.enabled === false) {
@ -82,13 +79,6 @@ export class ConfigDiagnostics {
}
}
private static async testEmailMessagingConfig(Messaging: MessagingConfig, config: PrivateConfigClass): Promise<void> {
Logger.debug(LOG_TAG, 'Testing EmailMessaging config');
if (Messaging.Email.type === EmailMessagingType.sendmail && ServerEnvironment.sendMailAvailable === false) {
throw new Error('sendmail e-mail sending method is not supported as the sendmail application cannot be found in the OS.');
}
}
static testVideoConfig(videoConfig: ServerVideoConfig,
config: PrivateConfigClass): Promise<void> {
@ -106,19 +96,19 @@ export class ConfigDiagnostics {
ffmpeg().getAvailableCodecs((err: Error) => {
if (err) {
return reject(
new Error(
'Error accessing ffmpeg, cant find executable: ' +
err.toString()
)
new Error(
'Error accessing ffmpeg, cant find executable: ' +
err.toString()
)
);
}
ffmpeg(__dirname + '/blank.jpg').ffprobe((err2: Error) => {
if (err2) {
return reject(
new Error(
'Error accessing ffmpeg-probe, cant find executable: ' +
err2.toString()
)
new Error(
'Error accessing ffmpeg-probe, cant find executable: ' +
err2.toString()
)
);
}
return resolve();
@ -166,7 +156,7 @@ export class ConfigDiagnostics {
static async testThumbnailConfig(
thumbnailConfig: ServerThumbnailConfig
thumbnailConfig: ServerThumbnailConfig
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing thumbnail config');
@ -177,7 +167,7 @@ export class ConfigDiagnostics {
if (isNaN(thumbnailConfig.iconSize) || thumbnailConfig.iconSize <= 0) {
throw new Error(
'IconSize has to be >= 0 integer, got: ' + thumbnailConfig.iconSize
'IconSize has to be >= 0 integer, got: ' + thumbnailConfig.iconSize
);
}
@ -192,16 +182,16 @@ export class ConfigDiagnostics {
}
static async testTasksConfig(
task: ServerJobConfig,
config: PrivateConfigClass
task: ServerJobConfig,
config: PrivateConfigClass
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing tasks config');
return;
}
static async testFacesConfig(
faces: ClientFacesConfig,
config: PrivateConfigClass
faces: ClientFacesConfig,
config: PrivateConfigClass
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing faces config');
if (faces.enabled === true) {
@ -212,29 +202,29 @@ export class ConfigDiagnostics {
}
static async testSearchConfig(
search: ClientSearchConfig,
config: PrivateConfigClass
search: ClientSearchConfig,
config: PrivateConfigClass
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing search config');
//nothing to check
}
static async testSharingConfig(
sharing: ClientSharingConfig,
config: PrivateConfigClass
sharing: ClientSharingConfig,
config: PrivateConfigClass
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing sharing config');
if (
sharing.enabled === true &&
config.Users.authenticationRequired === false
sharing.enabled === true &&
config.Users.authenticationRequired === false
) {
throw new Error('In case of no authentication, sharing is not supported');
}
}
static async testRandomPhotoConfig(
sharing: ClientRandomPhotoConfig,
config: PrivateConfigClass
sharing: ClientRandomPhotoConfig,
config: PrivateConfigClass
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing random photo config');
//nothing to check
@ -246,14 +236,14 @@ export class ConfigDiagnostics {
return;
}
if (
map.mapProvider === MapProviders.Mapbox &&
(!map.mapboxAccessToken || map.mapboxAccessToken.length === 0)
map.mapProvider === MapProviders.Mapbox &&
(!map.mapboxAccessToken || map.mapboxAccessToken.length === 0)
) {
throw new Error('Mapbox needs a valid api key.');
}
if (
map.mapProvider === MapProviders.Custom &&
(!map.customLayers || map.customLayers.length === 0)
map.mapProvider === MapProviders.Custom &&
(!map.customLayers || map.customLayers.length === 0)
) {
throw new Error('Custom maps need at least one valid layer');
}
@ -270,10 +260,10 @@ export class ConfigDiagnostics {
Logger.debug(LOG_TAG, 'Testing preview config');
const sp = new SearchQueryParser();
if (
!Utils.equalsFilter(
sp.parse(sp.stringify(settings.SearchQuery)),
settings.SearchQuery
)
!Utils.equalsFilter(
sp.parse(sp.stringify(settings.SearchQuery)),
settings.SearchQuery
)
) {
throw new Error('SearchQuery is not valid. Got: ' + JSON.stringify(sp.parse(sp.stringify(settings.SearchQuery))));
}
@ -296,26 +286,9 @@ export class ConfigDiagnostics {
await ConfigDiagnostics.testSharingConfig(config.Sharing, config);
await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config);
await ConfigDiagnostics.testMapConfig(config.Map);
await ConfigDiagnostics.testEmailMessagingConfig(config.Messaging, config);
}
static async checkAndSetEnvironment(): Promise<void> {
Logger.debug(LOG_TAG, 'Checking sendmail availability');
const transporter = createTransport({
sendmail: true,
});
try {
ServerEnvironment.sendMailAvailable = await transporter.verify();
} catch (e) {
ServerEnvironment.sendMailAvailable = false;
}
Config.Environment.sendMailAvailable = ServerEnvironment.sendMailAvailable;
if (!ServerEnvironment.sendMailAvailable) {
Config.Messaging.Email.type = EmailMessagingType.SMTP;
Logger.info(LOG_TAG, 'Sendmail is not available on the OS. You will need to use an SMTP server if you wish the app to send mails.');
}
}
static async runDiagnostics(): Promise<void> {
@ -323,21 +296,6 @@ export class ConfigDiagnostics {
NotificationManager.warning('You are running the application with NODE_ENV=debug. This exposes a lot of debug information that can be a security vulnerability. Set NODE_ENV=production, when you finished debugging.');
}
try {
await ConfigDiagnostics.checkAndSetEnvironment();
} catch (ex) {
const err: Error = ex;
NotificationManager.error(
'Error during checking environment',
err.toString()
);
Logger.error(
LOG_TAG,
'Error during checking environment',
err.toString()
);
process.exit(1);
}
try {
await ConfigDiagnostics.testDatabase(Config.Database);
@ -345,8 +303,8 @@ export class ConfigDiagnostics {
const err: Error = ex;
Logger.warn(LOG_TAG, '[SQL error]', err.toString());
Logger.error(
LOG_TAG,
'Error during initializing SQL DB, check DB connection and settings'
LOG_TAG,
'Error during initializing SQL DB, check DB connection and settings'
);
process.exit(1);
}
@ -357,15 +315,15 @@ export class ConfigDiagnostics {
const err: Error = ex;
Logger.warn(
LOG_TAG,
'[Thumbnail hardware acceleration] module error: ',
err.toString()
LOG_TAG,
'[Thumbnail hardware acceleration] module error: ',
err.toString()
);
Logger.warn(
LOG_TAG,
'Thumbnail hardware acceleration is not possible.' +
' \'sharp\' node module is not found.' +
' Falling back temporally to JS based thumbnail generation'
LOG_TAG,
'Thumbnail hardware acceleration is not possible.' +
' \'sharp\' node module is not found.' +
' Falling back temporally to JS based thumbnail generation'
);
process.exit(1);
}
@ -383,32 +341,32 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Video support error, switching off..',
err.toString()
'Video support error, switching off..',
err.toString()
);
Logger.warn(
LOG_TAG,
'Video support error, switching off..',
err.toString()
LOG_TAG,
'Video support error, switching off..',
err.toString()
);
Config.Media.Video.enabled = false;
}
try {
await ConfigDiagnostics.testMetaFileConfig(
Config.MetaFile,
Config
Config.MetaFile,
Config
);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Meta file support error, switching off gpx..',
err.toString()
'Meta file support error, switching off gpx..',
err.toString()
);
Logger.warn(
LOG_TAG,
'Meta file support error, switching off..',
err.toString()
LOG_TAG,
'Meta file support error, switching off..',
err.toString()
);
Config.MetaFile.gpx = false;
}
@ -418,13 +376,13 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Albums support error, switching off..',
err.toString()
'Albums support error, switching off..',
err.toString()
);
Logger.warn(
LOG_TAG,
'Meta file support error, switching off..',
err.toString()
LOG_TAG,
'Meta file support error, switching off..',
err.toString()
);
Config.Album.enabled = false;
}
@ -438,7 +396,7 @@ export class ConfigDiagnostics {
}
try {
await ConfigDiagnostics.testThumbnailConfig(
Config.Media.Thumbnail
Config.Media.Thumbnail
);
} catch (ex) {
const err: Error = ex;
@ -451,14 +409,14 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Search is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
'Search is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Search is not supported with these settings, switching off..',
err.toString()
LOG_TAG,
'Search is not supported with these settings, switching off..',
err.toString()
);
Config.Search.enabled = false;
}
@ -468,13 +426,13 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Preview settings are not valid, resetting search query',
err.toString()
'Preview settings are not valid, resetting search query',
err.toString()
);
Logger.warn(
LOG_TAG,
'Preview settings are not valid, resetting search query',
err.toString()
LOG_TAG,
'Preview settings are not valid, resetting search query',
err.toString()
);
Config.Preview.SearchQuery = {
type: SearchQueryTypes.any_text,
@ -487,14 +445,14 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Faces are not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
'Faces are not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Faces are not supported with these settings, switching off..',
err.toString()
LOG_TAG,
'Faces are not supported with these settings, switching off..',
err.toString()
);
Config.Faces.enabled = false;
}
@ -504,14 +462,14 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Some Tasks are not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
'Some Tasks are not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Some Tasks not supported with these settings, switching off..',
err.toString()
LOG_TAG,
'Some Tasks not supported with these settings, switching off..',
err.toString()
);
Config.Faces.enabled = false;
}
@ -521,34 +479,34 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Sharing is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
'Sharing is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Sharing is not supported with these settings, switching off..',
err.toString()
LOG_TAG,
'Sharing is not supported with these settings, switching off..',
err.toString()
);
Config.Sharing.enabled = false;
}
try {
await ConfigDiagnostics.testRandomPhotoConfig(
Config.Sharing,
Config
Config.Sharing,
Config
);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Random Media is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
'Random Media is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Random Media is not supported with these settings, switching off..',
err.toString()
LOG_TAG,
'Random Media is not supported with these settings, switching off..',
err.toString()
);
Config.Sharing.enabled = false;
}
@ -558,33 +516,18 @@ export class ConfigDiagnostics {
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Maps is not supported with these settings. Using open street maps temporally. ' +
'Please adjust the config properly.',
err.toString()
'Maps is not supported with these settings. Using open street maps temporally. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Maps is not supported with these settings. Using open street maps temporally ' +
'Please adjust the config properly.',
err.toString()
LOG_TAG,
'Maps is not supported with these settings. Using open street maps temporally ' +
'Please adjust the config properly.',
err.toString()
);
Config.Map.mapProvider = MapProviders.OpenStreetMap;
}
try {
await ConfigDiagnostics.testEmailMessagingConfig(Config.Messaging, Config);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Setting to SMTP method.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Setting to SMTP method.',
err.toString()
);
Config.Messaging.Email.type = EmailMessagingType.SMTP;
}
}

View File

@ -1,7 +1,6 @@
import {createTransport, Transporter} from 'nodemailer';
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
import {Config} from '../../../common/config/private/Config';
import {EmailMessagingType} from '../../../common/config/private/MessagingConfig';
import {PhotoProcessing} from '../fileprocessing/PhotoProcessing';
import {ThumbnailSourceType} from '../threading/PhotoWorker';
import {ProjectPath} from '../../ProjectPath';
@ -14,11 +13,6 @@ export class EmailMediaMessenger {
transporter: Transporter;
constructor() {
if (Config.Messaging.Email.type === EmailMessagingType.sendmail) {
this.transporter = createTransport({
sendmail: true
});
} else {
this.transporter = createTransport({
host: Config.Messaging.Email.smtp.host,
port: Config.Messaging.Email.smtp.port,
@ -29,8 +23,6 @@ export class EmailMediaMessenger {
pass: Config.Messaging.Email.smtp.password
}
});
}
}
private async getThumbnail(m: MediaDTO) {

View File

@ -6,8 +6,6 @@ import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node';
import {IConfigClass} from 'typeconfig/common';
import {PasswordHelper} from '../../../backend/model/PasswordHelper';
import {TAGS} from '../public/ClientConfig';
import {ServerEnvironment} from '../../../backend/Environment';
import {EmailMessagingType} from './MessagingConfig';
declare const process: any;
@ -85,12 +83,6 @@ export class PrivateConfigClass extends ServerConfig {
require('../../../../package.json').buildCommitHash;
this.Environment.upTime = upTime;
this.Environment.isDocker = !!process.env.PI_DOCKER;
if (typeof ServerEnvironment.sendMailAvailable !== 'undefined') {
this.Environment.sendMailAvailable = ServerEnvironment.sendMailAvailable;
if (!this.Environment.sendMailAvailable) { //onNewValue is not yet available as a callback
this.Messaging.Email.type = EmailMessagingType.SMTP;
}
}
}
async original(): Promise<PrivateConfigClass & IConfigClass> {

View File

@ -12,10 +12,6 @@ if (typeof $localize === 'undefined') {
global.$localize = (s) => s;
}
export enum EmailMessagingType {
sendmail = 1,
SMTP = 2,
}
@SubConfigClass<TAGS>({softReadonly: true})
export class EmailSMTPMessagingConfig {
@ -81,19 +77,8 @@ export class EmailSMTPMessagingConfig {
@SubConfigClass<TAGS>({softReadonly: true})
export class EmailMessagingConfig {
@ConfigProperty<EmailMessagingType, EmailMessagingConfig>({
type: EmailMessagingType,
tags:
{
name: $localize`Sending method`,
priority: ConfigPriority.advanced,
uiDisabled: (sc: EmailMessagingConfig, c: ServerConfig) => !c.Environment.sendMailAvailable
} as TAGS,
description: $localize`Sendmail uses the built in unix binary if available. STMP connects to any STMP server of your choice.`
})
type: EmailMessagingType = EmailMessagingType.sendmail;
@ConfigProperty<EmailMessagingType, EmailMessagingConfig>({
@ConfigProperty({
tags:
{
name: $localize`Sender email`,
@ -107,7 +92,6 @@ export class EmailMessagingConfig {
tags:
{
name: $localize`SMTP`,
relevant: (c: any) => c.type === EmailMessagingType.SMTP,
}
})
smtp?: EmailSMTPMessagingConfig = new EmailSMTPMessagingConfig();

View File

@ -30,8 +30,8 @@ import {DefaultsJobs} from '../../entities/job/JobDTO';
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO';
import {SortingMethods} from '../../entities/SortingMethods';
import {UserRoles} from '../../entities/UserDTO';
import {EmailMessagingType, MessagingConfig} from './MessagingConfig';
import {MediaPickDTO} from '../../entities/MediaPickDTO';
import {MessagingConfig} from './MessagingConfig';
declare let $localize: (s: TemplateStringsArray) => string;
@ -83,14 +83,14 @@ export enum FFmpegPresets {
export type videoCodecType = 'libvpx-vp9' | 'libx264' | 'libvpx' | 'libx265';
export type videoResolutionType =
| 240
| 360
| 480
| 720
| 1080
| 1440
| 2160
| 4320;
| 240
| 360
| 480
| 720
| 1080
| 1440
| 2160
| 4320;
export type videoFormatType = 'mp4' | 'webm';
@SubConfigClass({softReadonly: true})
@ -98,51 +98,51 @@ export class MySQLConfig {
@ConfigProperty({
envAlias: 'MYSQL_HOST',
tags:
{
name: $localize`Host`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
{
name: $localize`Host`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
})
host: string = 'localhost';
@ConfigProperty({
envAlias: 'MYSQL_PORT', min: 0, max: 65535,
tags:
{
name: $localize`Port`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
{
name: $localize`Port`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
})
port: number = 3306;
@ConfigProperty({
envAlias: 'MYSQL_DATABASE',
tags:
{
name: $localize`Database`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
{
name: $localize`Database`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
})
database: string = 'pigallery2';
@ConfigProperty({
envAlias: 'MYSQL_USERNAME',
tags:
{
name: $localize`Username`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
{
name: $localize`Username`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
})
username: string = '';
@ConfigProperty({
envAlias: 'MYSQL_PASSWORD', type: 'password',
tags:
{
name: $localize`Password`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
}
{
name: $localize`Password`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
}
})
password: string = '';
}
@ -151,11 +151,11 @@ export class MySQLConfig {
export class SQLiteConfig {
@ConfigProperty({
tags:
{
name: $localize`Sqlite db filename`,
uiResetNeeded: {server: true},
priority: ConfigPriority.underTheHood
},
{
name: $localize`Sqlite db filename`,
uiResetNeeded: {server: true},
priority: ConfigPriority.underTheHood
},
description: $localize`Sqlite will save the db with this filename.`,
})
DBFileName: string = 'sqlite.db';
@ -165,51 +165,51 @@ export class SQLiteConfig {
export class UserConfig {
@ConfigProperty({
tags:
{
name: $localize`Name`,
priority: ConfigPriority.underTheHood
}
{
name: $localize`Name`,
priority: ConfigPriority.underTheHood
}
})
name: string;
@ConfigProperty({
type: UserRoles,
tags:
{
name: $localize`Role`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Role`,
priority: ConfigPriority.underTheHood
},
})
role: UserRoles = UserRoles.User;
@ConfigProperty<string, ServerConfig, TAGS>({
type: 'string',
tags:
{
name: $localize`Password`,
priority: ConfigPriority.underTheHood,
relevant: (c: UserConfig) => !c.encrypted
},
{
name: $localize`Password`,
priority: ConfigPriority.underTheHood,
relevant: (c: UserConfig) => !c.encrypted
},
description: $localize`Unencrypted, temporary password. App will encrypt it and delete this.`
})
password: string;
@ConfigProperty({
tags:
{
name: $localize`Encrypted password`,
priority: ConfigPriority.underTheHood,
secret: true
},
{
name: $localize`Encrypted password`,
priority: ConfigPriority.underTheHood,
secret: true
},
})
encryptedPassword: string | undefined;
@ConfigProperty({
tags:
{
priority: ConfigPriority.underTheHood,
relevant: () => false // never render this on UI. Only used to indicate that encryption is done.
} as TAGS,
{
priority: ConfigPriority.underTheHood,
relevant: () => false // never render this on UI. Only used to indicate that encryption is done.
} as TAGS,
})
encrypted: boolean;
@ -231,44 +231,44 @@ export class ServerDataBaseConfig {
@ConfigProperty<DatabaseType, ServerConfig>({
type: DatabaseType,
tags:
{
name: $localize`Type`,
priority: ConfigPriority.advanced,
uiResetNeeded: {db: true},
githubIssue: 573
} as TAGS,
{
name: $localize`Type`,
priority: ConfigPriority.advanced,
uiResetNeeded: {db: true},
githubIssue: 573
} as TAGS,
description: $localize`SQLite is recommended.`
})
type: DatabaseType = DatabaseType.sqlite;
@ConfigProperty({
tags:
{
name: $localize`Database folder`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
{
name: $localize`Database folder`,
uiResetNeeded: {server: true},
priority: ConfigPriority.advanced
},
description: $localize`All file-based data will be stored here (sqlite database, job history data).`,
})
dbFolder: string = 'db';
@ConfigProperty({
tags:
{
name: $localize`SQLite`,
uiResetNeeded: {db: true},
relevant: (c: any) => c.type === DatabaseType.sqlite,
}
{
name: $localize`SQLite`,
uiResetNeeded: {db: true},
relevant: (c: any) => c.type === DatabaseType.sqlite,
}
})
sqlite?: SQLiteConfig = new SQLiteConfig();
@ConfigProperty({
tags:
{
name: $localize`MySQL`,
uiResetNeeded: {db: true},
relevant: (c: any) => c.type === DatabaseType.mysql,
}
{
name: $localize`MySQL`,
uiResetNeeded: {db: true},
relevant: (c: any) => c.type === DatabaseType.mysql,
}
})
mysql?: MySQLConfig = new MySQLConfig();
@ -281,13 +281,13 @@ export class ServerUserConfig extends ClientUserConfig {
@ConfigProperty({
arrayType: UserConfig,
tags:
{
name: $localize`Enforced users`,
priority: ConfigPriority.underTheHood,
uiResetNeeded: {server: true},
uiOptional: true,
githubIssue: 575
} as TAGS,
{
name: $localize`Enforced users`,
priority: ConfigPriority.underTheHood,
uiResetNeeded: {server: true},
uiOptional: true,
githubIssue: 575
} as TAGS,
description: $localize`Creates these users in the DB during startup if they do not exist. If a user with this name exist, it won't be overwritten, even if the role is different.`,
})
enforcedUsers: UserConfig[] = [];
@ -298,40 +298,40 @@ export class ServerUserConfig extends ClientUserConfig {
export class ServerThumbnailConfig extends ClientThumbnailConfig {
@ConfigProperty({
tags:
{
name: $localize`Enforced users`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Enforced users`,
priority: ConfigPriority.underTheHood
},
description: $localize`if true, 'lanczos3' will used to scale photos, otherwise faster but lower quality 'nearest'.`
})
useLanczos3: boolean = true;
@ConfigProperty({
max: 100, min: 1, type: 'unsignedInt',
tags:
{
name: $localize`Converted photo and thumbnail quality`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Converted photo and thumbnail quality`,
priority: ConfigPriority.underTheHood
},
description: $localize`Between 0-100.`
})
quality = 80;
@ConfigProperty({
type: 'boolean',
tags:
{
name: $localize`Use chroma subsampling.`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Use chroma subsampling.`,
priority: ConfigPriority.underTheHood
},
description: $localize`Use high quality chroma subsampling in webp. See: https://sharp.pixelplumbing.com/api-output#webp.`
})
smartSubsample = true;
@ConfigProperty({
type: 'ratio',
tags:
{
name: $localize`Person face margin`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Person face margin`,
priority: ConfigPriority.underTheHood
},
description: $localize`Person face size ratio on the face thumbnail.`
})
personFaceMargin: number = 0.6; // in ration [0-1]
@ -341,47 +341,47 @@ export class ServerThumbnailConfig extends ClientThumbnailConfig {
export class ServerGPXCompressingConfig extends ClientGPXCompressingConfig {
@ConfigProperty({
tags:
{
name: $localize`OnTheFly *.gpx compression`,
priority: ConfigPriority.advanced,
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
},
{
name: $localize`OnTheFly *.gpx compression`,
priority: ConfigPriority.advanced,
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
},
description: $localize`Enables on the fly *.gpx compression.`,
})
onTheFly: boolean = true;
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Min distance`,
priority: ConfigPriority.underTheHood,
unit: 'm',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
} as TAGS,
{
name: $localize`Min distance`,
priority: ConfigPriority.underTheHood,
unit: 'm',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
} as TAGS,
description: $localize`Filters out entry that are closer than this to each other in meters.`
})
minDistance: number = 5;
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Max middle point deviance`,
priority: ConfigPriority.underTheHood,
unit: 'm',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
} as TAGS,
{
name: $localize`Max middle point deviance`,
priority: ConfigPriority.underTheHood,
unit: 'm',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
} as TAGS,
description: $localize`Filters out entry that would fall on the line if we would just connect the previous and the next points. This setting sets the sensitivity for that (higher number, more points are filtered).`
})
maxMiddleDeviance: number = 5;
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Min time delta`,
priority: ConfigPriority.underTheHood,
unit: 'ms',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
} as TAGS,
{
name: $localize`Min time delta`,
priority: ConfigPriority.underTheHood,
unit: 'ms',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
} as TAGS,
description: $localize`Filters out entry that are closer than this in time in milliseconds.`
})
minTimeDistance: number = 5000;
@ -391,17 +391,17 @@ export class ServerGPXCompressingConfig extends ClientGPXCompressingConfig {
export class ServerMetaFileConfig extends ClientMetaFileConfig {
@ConfigProperty({
tags:
{
name: $localize`GPX compression`,
priority: ConfigPriority.advanced,
uiJob: [{
job: DefaultsJobs[DefaultsJobs['GPX Compression']],
relevant: (c) => c.MetaFile.GPXCompressing.enabled
}, {
job: DefaultsJobs[DefaultsJobs['Delete Compressed GPX']],
relevant: (c) => c.MetaFile.GPXCompressing.enabled
}]
} as TAGS
{
name: $localize`GPX compression`,
priority: ConfigPriority.advanced,
uiJob: [{
job: DefaultsJobs[DefaultsJobs['GPX Compression']],
relevant: (c) => c.MetaFile.GPXCompressing.enabled
}, {
job: DefaultsJobs[DefaultsJobs['Delete Compressed GPX']],
relevant: (c) => c.MetaFile.GPXCompressing.enabled
}]
} as TAGS
})
GPXCompressing: ServerGPXCompressingConfig = new ServerGPXCompressingConfig();
}
@ -412,11 +412,11 @@ export class ServerSharingConfig extends ClientSharingConfig {
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Update timeout`,
priority: ConfigPriority.underTheHood,
unit: 'ms'
} as TAGS,
{
name: $localize`Update timeout`,
priority: ConfigPriority.underTheHood,
unit: 'ms'
} as TAGS,
description: $localize`After creating a sharing link, it can be updated for this long.`
})
updateTimeout: number = 1000 * 60 * 5;
@ -427,47 +427,47 @@ export class ServerIndexingConfig {
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Index cache timeout`,
priority: ConfigPriority.underTheHood,
unit: 'ms'
} as TAGS,
{
name: $localize`Index cache timeout`,
priority: ConfigPriority.underTheHood,
unit: 'ms'
} as TAGS,
description: $localize`If there was no indexing in this time, it reindexes. (skipped if indexes are in DB and sensitivity is low).`
})
cachedFolderTimeout: number = 1000 * 60 * 60; // Do not rescans the folder if seems ok
@ConfigProperty({
type: ReIndexingSensitivity,
tags:
{
name: $localize`Folder reindexing sensitivity`,
priority: ConfigPriority.advanced
},
{
name: $localize`Folder reindexing sensitivity`,
priority: ConfigPriority.advanced
},
description: $localize`Set the reindexing sensitivity. High value check the folders for change more often.`
})
reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low;
@ConfigProperty({
arrayType: 'string',
tags:
{
name: $localize`Exclude Folder List`,
priority: ConfigPriority.advanced,
uiResetNeeded: {server: true, db: true},
uiOptional: true,
uiAllowSpaces: true
} as TAGS,
{
name: $localize`Exclude Folder List`,
priority: ConfigPriority.advanced,
uiResetNeeded: {server: true, db: true},
uiOptional: true,
uiAllowSpaces: true
} as TAGS,
description: $localize`Folders to exclude from indexing. If an entry starts with '/' it is treated as an absolute path. If it doesn't start with '/' but contains a '/', the path is relative to the image directory. If it doesn't contain a '/', any folder with this name will be excluded.`,
})
excludeFolderList: string[] = ['.Trash-1000', '.dtrash', '$RECYCLE.BIN'];
@ConfigProperty({
arrayType: 'string',
tags:
{
name: $localize`Exclude File List`,
priority: ConfigPriority.advanced,
uiResetNeeded: {server: true, db: true},
uiOptional: true,
hint: $localize`.ignore;.pg2ignore`
} as TAGS,
{
name: $localize`Exclude File List`,
priority: ConfigPriority.advanced,
uiResetNeeded: {server: true, db: true},
uiOptional: true,
hint: $localize`.ignore;.pg2ignore`
} as TAGS,
description: $localize`Files that mark a folder to be excluded from indexing. Any folder that contains a file with this name will be excluded from indexing.`,
})
excludeFileList: string[] = [];
@ -477,21 +477,21 @@ export class ServerIndexingConfig {
export class ServerThreadingConfig {
@ConfigProperty({
tags:
{
name: $localize`Threading`,
uiResetNeeded: {server: true},
priority: ConfigPriority.underTheHood,
} as TAGS,
{
name: $localize`Threading`,
uiResetNeeded: {server: true},
priority: ConfigPriority.underTheHood,
} as TAGS,
description: $localize`[Deprecated, will be removed in the next release] Runs directory scanning and thumbnail generation in a different thread.`
})
enabled: boolean = false;
@ConfigProperty({
tags:
{
name: $localize`Thumbnail threads`,
uiResetNeeded: {server: true},
priority: ConfigPriority.underTheHood
},
{
name: $localize`Thumbnail threads`,
uiResetNeeded: {server: true},
priority: ConfigPriority.underTheHood
},
description: $localize`Number of threads that are used to generate thumbnails. If 0, number of 'CPU cores -1' threads will be used.`,
})
thumbnailThreads: number = 0; // if zero-> CPU count -1
@ -502,10 +502,10 @@ export class ServerDuplicatesConfig {
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Max duplicates`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Max duplicates`,
priority: ConfigPriority.underTheHood
},
description: $localize`Maximum number of duplicates to list.`
})
listingLimit: number = 1000;
@ -606,21 +606,21 @@ export class JobScheduleConfig implements JobScheduleDTO {
},
})
trigger:
| AfterJobTriggerConfig
| NeverJobTriggerConfig
| PeriodicJobTriggerConfig
| ScheduledJobTriggerConfig;
constructor(
name: string,
jobName: string,
trigger:
| AfterJobTriggerConfig
| NeverJobTriggerConfig
| PeriodicJobTriggerConfig
| ScheduledJobTriggerConfig,
config: any = {},
allowParallelRun: boolean = false
| ScheduledJobTriggerConfig;
constructor(
name: string,
jobName: string,
trigger:
| AfterJobTriggerConfig
| NeverJobTriggerConfig
| PeriodicJobTriggerConfig
| ScheduledJobTriggerConfig,
config: any = {},
allowParallelRun: boolean = false
) {
this.name = name;
this.jobName = jobName;
@ -635,20 +635,20 @@ export class ServerJobConfig {
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Max saved progress`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Max saved progress`,
priority: ConfigPriority.underTheHood
},
description: $localize`Job history size.`
})
maxSavedProgress: number = 20;
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Processing batch size`,
priority: ConfigPriority.underTheHood
},
{
name: $localize`Processing batch size`,
priority: ConfigPriority.underTheHood
},
description: $localize`Jobs load this many photos or videos form the DB for processing at once.`
})
mediaProcessingBatchSize: number = 1000;
@ -661,46 +661,46 @@ export class ServerJobConfig {
})
scheduled: JobScheduleConfig[] = [
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs.Indexing],
DefaultsJobs[DefaultsJobs.Indexing],
new NeverJobTriggerConfig(),
{indexChangesOnly: true} // set config explicitly, so it is not undefined on the UI
DefaultsJobs[DefaultsJobs.Indexing],
DefaultsJobs[DefaultsJobs.Indexing],
new NeverJobTriggerConfig(),
{indexChangesOnly: true} // set config explicitly, so it is not undefined on the UI
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Preview Filling']],
DefaultsJobs[DefaultsJobs['Preview Filling']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Indexing']]),
{}
DefaultsJobs[DefaultsJobs['Preview Filling']],
DefaultsJobs[DefaultsJobs['Preview Filling']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Indexing']]),
{}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Preview Filling']]),
{sizes: [240], indexedOnly: true}
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Preview Filling']]),
{sizes: [240], indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Photo Converting']],
DefaultsJobs[DefaultsJobs['Photo Converting']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
{indexedOnly: true}
DefaultsJobs[DefaultsJobs['Photo Converting']],
DefaultsJobs[DefaultsJobs['Photo Converting']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
{indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Video Converting']],
DefaultsJobs[DefaultsJobs['Video Converting']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Photo Converting']]),
{indexedOnly: true}
DefaultsJobs[DefaultsJobs['Video Converting']],
DefaultsJobs[DefaultsJobs['Video Converting']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Photo Converting']]),
{indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['GPX Compression']],
DefaultsJobs[DefaultsJobs['GPX Compression']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Video Converting']]),
{indexedOnly: true}
DefaultsJobs[DefaultsJobs['GPX Compression']],
DefaultsJobs[DefaultsJobs['GPX Compression']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Video Converting']]),
{indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['GPX Compression']]),
{indexedOnly: true}
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['GPX Compression']]),
{indexedOnly: true}
),
];
}
@ -710,73 +710,73 @@ export class VideoTranscodingConfig {
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Bit rate`,
priority: ConfigPriority.advanced,
unit: 'bps'
},
{
name: $localize`Bit rate`,
priority: ConfigPriority.advanced,
unit: 'bps'
},
description: $localize`Target bit rate of the output video will be scaled down this this. This should be less than the upload rate of your home server.`
})
bitRate: number = 5 * 1024 * 1024;
@ConfigProperty({
type: 'unsignedInt',
tags:
{
name: $localize`Resolution`,
priority: ConfigPriority.advanced,
uiOptions: [720, 1080, 1440, 2160, 4320],
unit: 'px'
},
{
name: $localize`Resolution`,
priority: ConfigPriority.advanced,
uiOptions: [720, 1080, 1440, 2160, 4320],
unit: 'px'
},
description: $localize`The height of the output video will be scaled down to this, while keeping the aspect ratio.`
})
resolution: videoResolutionType = 720;
@ConfigProperty({
type: 'positiveFloat',
tags:
{
name: $localize`FPS`,
priority: ConfigPriority.underTheHood,
uiOptions: [24, 25, 30, 48, 50, 60]
},
{
name: $localize`FPS`,
priority: ConfigPriority.underTheHood,
uiOptions: [24, 25, 30, 48, 50, 60]
},
description: $localize`Target frame per second (fps) of the output video will be scaled down this this.`
})
fps: number = 25;
@ConfigProperty({
tags:
{
name: $localize`Format`,
priority: ConfigPriority.advanced,
uiOptions: ['mp4', 'webm']
}
{
name: $localize`Format`,
priority: ConfigPriority.advanced,
uiOptions: ['mp4', 'webm']
}
})
format: videoFormatType = 'mp4';
@ConfigProperty({
tags:
{
name: $localize`MP4 codec`,
priority: ConfigPriority.underTheHood,
uiOptions: ['libx264', 'libx265'],
relevant: (c: any) => c.format === 'mp4'
}
{
name: $localize`MP4 codec`,
priority: ConfigPriority.underTheHood,
uiOptions: ['libx264', 'libx265'],
relevant: (c: any) => c.format === 'mp4'
}
})
mp4Codec: videoCodecType = 'libx264';
@ConfigProperty({
tags:
{
name: $localize`Webm Codec`,
priority: ConfigPriority.underTheHood,
uiOptions: ['libvpx', 'libvpx-vp9'],
relevant: (c: any) => c.format === 'webm'
}
{
name: $localize`Webm Codec`,
priority: ConfigPriority.underTheHood,
uiOptions: ['libvpx', 'libvpx-vp9'],
relevant: (c: any) => c.format === 'webm'
}
})
webmCodec: videoCodecType = 'libvpx';
@ConfigProperty({
type: 'unsignedInt', max: 51,
tags:
{
name: $localize`CRF`,
priority: ConfigPriority.underTheHood,
},
{
name: $localize`CRF`,
priority: ConfigPriority.underTheHood,
},
description: $localize`The range of the Constant Rate Factor (CRF) scale is 0–51, where 0 is lossless, 23 is the default, and 51 is worst quality possible.`,
})
@ -784,10 +784,10 @@ export class VideoTranscodingConfig {
@ConfigProperty({
type: FFmpegPresets,
tags:
{
name: $localize`Preset`,
priority: ConfigPriority.advanced,
},
{
name: $localize`Preset`,
priority: ConfigPriority.advanced,
},
description: $localize`A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower preset will provide better compression (compression is quality per filesize).`,
})
preset: FFmpegPresets = FFmpegPresets.medium;
@ -839,7 +839,7 @@ export class PhotoConvertingConfig extends ClientPhotoConvertingConfig {
name: $localize`On the fly converting`,
priority: ConfigPriority.underTheHood,
uiDisabled: (sc: PhotoConvertingConfig) =>
!sc.enabled
!sc.enabled
},
description: $localize`Converts photos on the fly, when they are requested.`,
@ -853,7 +853,7 @@ export class PhotoConvertingConfig extends ClientPhotoConvertingConfig {
uiOptions: [720, 1080, 1440, 2160, 4320],
unit: 'px',
uiDisabled: (sc: PhotoConvertingConfig) =>
!sc.enabled
!sc.enabled
},
description: $localize`The shorter edge of the converted photo will be scaled down to this, while keeping the aspect ratio.`,
})
@ -1053,16 +1053,6 @@ export class ServerEnvironmentConfig {
buildCommitHash: string | undefined;
@ConfigProperty({volatile: true})
isDocker: boolean | undefined;
@ConfigProperty<boolean, ServerConfig, TAGS>({
volatile: true,
onNewValue: (value, config) => {
if (value === false) {
config.Messaging.Email.type = EmailMessagingType.SMTP;
}
},
description: 'App updates on start-up if sendmail binary is available'
})
sendMailAvailable: boolean | undefined;
}

View File

@ -45,9 +45,7 @@ describe('SettingsRouter', () => {
result.body.should.be.a('object');
should.equal(result.body.error, null);
(result.body.result as ServerConfig).Environment.upTime = null;
(result.body.result as ServerConfig).Environment.sendMailAvailable = null;
originalSettings.Environment.upTime = null;
originalSettings.Environment.sendMailAvailable = null;
result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({
attachState: true,
attachVolatile: true,

View File

@ -7,8 +7,6 @@ import {ServerUserConfig} from '../../../../../src/common/config/private/Private
import {Config} from '../../../../../src/common/config/private/Config';
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
import {ConfigClassBuilder} from '../../../../../node_modules/typeconfig/node';
import {ServerEnvironment} from '../../../../../src/backend/Environment';
import {EmailMessagingType} from '../../../../../src/common/config/private/MessagingConfig';
import * as fs from 'fs';
import * as path from 'path';
@ -26,9 +24,6 @@ describe('Settings middleware', () => {
});
it('should save empty enforced users settings', (done: (err?: any) => void) => {
ServerEnvironment.sendMailAvailable = false;
Config.Environment.sendMailAvailable = false;
Config.Messaging.Email.type = EmailMessagingType.SMTP;
const req: any = {
session: {},
sessionOptions: {},
@ -56,9 +51,6 @@ describe('Settings middleware', () => {
});
it('should save enforced users settings', (done: (err?: any) => void) => {
ServerEnvironment.sendMailAvailable = false;
Config.Environment.sendMailAvailable = false;
Config.Messaging.Email.type = EmailMessagingType.SMTP;
const req: any = {
session: {},
sessionOptions: {},