mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[feature] Notification service added
This commit is contained in:
@ -16,13 +16,38 @@
|
||||
"visibilityTimeout": 300,
|
||||
"retentionPeriod": 900
|
||||
},
|
||||
"mail": {
|
||||
"smtpConnectionConfiguration": {
|
||||
"email": {
|
||||
"smtpServerConfiguration": {
|
||||
"host": "smtp.ethereal.email",
|
||||
"port": 587,
|
||||
"auth": {
|
||||
"user": "onlyoffice",
|
||||
"pass": "onlyoffice"
|
||||
}
|
||||
},
|
||||
"messageDefaults": {
|
||||
"from": "some.mail@server.com",
|
||||
"connectionConfiguration": {
|
||||
"disableFileAccess": false,
|
||||
"disableUrlAccess": false
|
||||
},
|
||||
"contactDefaults": {
|
||||
"from": "from.mail@server.com"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"rules": {
|
||||
"licenseExpired": {
|
||||
"transportType": [
|
||||
"email",
|
||||
"telegram"
|
||||
],
|
||||
"template": {
|
||||
"title": "License",
|
||||
"body": "license expires after %s!!!"
|
||||
},
|
||||
"policies": {
|
||||
"repeatInterval": "1d"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
|
||||
13
Common/npm-shrinkwrap.json
generated
13
Common/npm-shrinkwrap.json
generated
@ -915,6 +915,13 @@
|
||||
"@smithy/util-retry": "^1.0.4",
|
||||
"tslib": "^2.5.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@smithy/middleware-serde": {
|
||||
@ -1980,9 +1987,9 @@
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"request-filtering-agent": "1.0.5",
|
||||
"rhea": "1.0.24",
|
||||
"uri-js": "4.2.2",
|
||||
"uuid": "9.0.1",
|
||||
"win-ca": "3.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,9 +35,11 @@
|
||||
const config = require('config');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const cfgConnection = config.get('mail.smtpConnectionConfiguration');
|
||||
const cfgMessageDefaults = config.get('mail.messageDefaults');
|
||||
const operationContext = require('./operationContext');
|
||||
|
||||
const cfgConnection = config.get('email.connectionConfiguration');
|
||||
|
||||
const ctx = new operationContext.Context();
|
||||
const connectionDefaultSettings = {
|
||||
pool: true,
|
||||
socketTimeout: 1000 * 60 * 2,
|
||||
@ -48,7 +50,7 @@ const connectionDefaultSettings = {
|
||||
const settings = Object.assign(connectionDefaultSettings, cfgConnection);
|
||||
const smtpTransporters = new Map();
|
||||
|
||||
function createTransporter(ctx, host, port, auth, messageCommonParameters = {}) {
|
||||
function createTransporter(host, port, auth, messageCommonParameters = {}) {
|
||||
const server = {
|
||||
host,
|
||||
port,
|
||||
@ -56,10 +58,13 @@ function createTransporter(ctx, host, port, auth, messageCommonParameters = {})
|
||||
secure: port === 465
|
||||
};
|
||||
const transport = Object.assign({}, server, settings);
|
||||
const mailDefaults = Object.assign({}, cfgMessageDefaults, messageCommonParameters);
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport(transport, mailDefaults);
|
||||
if (smtpTransporters.has(`${host}:${auth.user}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(transport, messageCommonParameters);
|
||||
smtpTransporters.set(`${host}:${auth.user}`, transporter);
|
||||
} catch (error) {
|
||||
ctx.logger.error('Mail service smtp transporter creation error: %o\nWith parameters: \n\thost - %s, \n\tport - %d, \n\tauth = %o', error.stack, host, port, auth);
|
||||
@ -75,7 +80,7 @@ async function send(host, userLogin, mailObject) {
|
||||
return transporter.sendMail(mailObject);
|
||||
}
|
||||
|
||||
function deleteTransporter(ctx, host, userLogin) {
|
||||
function deleteTransporter(host, userLogin) {
|
||||
const transporter = smtpTransporters.get(`${host}:${userLogin}`);
|
||||
if (!transporter) {
|
||||
ctx.logger.error(`MailService: no transporter exists for host "${host}" and user "${userLogin}"`);
|
||||
@ -91,6 +96,10 @@ function transportersRelease() {
|
||||
smtpTransporters.clear();
|
||||
}
|
||||
|
||||
function isCreated(host, user) {
|
||||
return smtpTransporters
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createTransporter,
|
||||
send,
|
||||
|
||||
173
Common/sources/notificationService.js
Normal file
173
Common/sources/notificationService.js
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2023
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
const config = require('config');
|
||||
const ms = require('ms');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const mailService = require('./mailService');
|
||||
const operationContext = require('./operationContext');
|
||||
|
||||
const cfgMailServer = config.get('email.smtpServerConfiguration');
|
||||
const cfgMailMessageDefaults = config.get('email.contactDefaults');
|
||||
|
||||
const uuidNamespace = 'e071294c-e621-4195-b453-6da9344e5c72';
|
||||
const ctx = new operationContext.Context();
|
||||
const defaultLicenseRepeatInterval = 1000 * 60 * 60 * 24;
|
||||
const recipients = new Map();
|
||||
const notificationTypes = {
|
||||
LICENSE_EXPIRED: 0,
|
||||
};
|
||||
|
||||
class TransportInterface {
|
||||
getRecipientId(messageParams) {}
|
||||
async send(message) {}
|
||||
contentGeneration(template, messageParams) {}
|
||||
}
|
||||
|
||||
class MailTransport extends TransportInterface {
|
||||
host = cfgMailServer.host;
|
||||
port = cfgMailServer.port;
|
||||
auth = cfgMailServer.auth;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
mailService.createTransporter(this.host, this.port, this.auth, cfgMailMessageDefaults);
|
||||
}
|
||||
|
||||
getRecipientId(messageParams) {
|
||||
if (!messageParams.to) {
|
||||
return uuid.NIL;
|
||||
}
|
||||
|
||||
return uuid.v5(messageParams.to, uuidNamespace);
|
||||
}
|
||||
|
||||
send(message) {
|
||||
ctx.logger.info('!!!!!!!!!!!!!!!!!!!!!SENDONG:', message);
|
||||
return mailService.send(this.host, this.auth.user, message);
|
||||
}
|
||||
|
||||
contentGeneration(template, messageParams) {
|
||||
const messageBody = {
|
||||
subject: template.title,
|
||||
text: template.body
|
||||
};
|
||||
|
||||
return Object.assign({}, messageBody, messageParams);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
class TelegramTransport extends TransportInterface {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class Transport {
|
||||
transport = new TransportInterface();
|
||||
|
||||
constructor(transportName) {
|
||||
this.name = transportName;
|
||||
|
||||
switch (transportName) {
|
||||
case 'email':
|
||||
this.transport = new MailTransport();
|
||||
break;
|
||||
case 'telegram':
|
||||
this.transport = new TelegramTransport();
|
||||
break
|
||||
default:
|
||||
ctx.logger.error(`Notification service error: transport method "${transportName}" not implemented`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRecipientData(id, defaultPoliciesData) {
|
||||
if (id === uuid.NIL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientData = recipients.get(id);
|
||||
if (!recipientData) {
|
||||
recipients.set(id, defaultPoliciesData);
|
||||
return defaultPoliciesData;
|
||||
}
|
||||
|
||||
return recipientData;
|
||||
}
|
||||
|
||||
async function notify(notificationType, messageParams) {
|
||||
ctx.logger.info('!!!!!!!!!!!!!!!!!!!!!HERE:', messageParams);
|
||||
switch (notificationType) {
|
||||
case notificationTypes.LICENSE_EXPIRED: {
|
||||
licenseExpiredNotify(messageParams);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function licenseExpiredNotify(messageParams) {
|
||||
const cfgLicenseExpired = config.get('notification.rules.licenseExpired');
|
||||
const transportObjects = cfgLicenseExpired.transportType.map(transport => new Transport(transport));
|
||||
const { repeatInterval } = cfgLicenseExpired.policies;
|
||||
const intervalMilliseconds = ms(repeatInterval) ?? defaultLicenseRepeatInterval;
|
||||
const defaultPolices = {
|
||||
repeatDate: Date.now(),
|
||||
}
|
||||
|
||||
transportObjects.forEach(object => {
|
||||
const recipientId = object.transport.getRecipientId(messageParams);
|
||||
const data = getRecipientData(recipientId, defaultPolices);
|
||||
|
||||
if (!data) {
|
||||
ctx.logger.error(`Notification service error, licenseExpiredNotify() transport "${object.name}": missing recipient data in message object \n${messageParams}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDateMs = Date.now();
|
||||
if (data.repeatDate <= currentDateMs) {
|
||||
data.repeatDate = currentDateMs + intervalMilliseconds;
|
||||
|
||||
const message = object.transport.contentGeneration(cfgLicenseExpired.template, messageParams);
|
||||
object.transport.send(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notificationTypes,
|
||||
notify
|
||||
};
|
||||
@ -1,20 +1,19 @@
|
||||
const { describe, test, expect, afterAll} = require('@jest/globals');
|
||||
const { describe, test, expect, afterAll } = require('@jest/globals');
|
||||
const nodemailer = require('../../Common/node_modules/nodemailer');
|
||||
|
||||
const mailService = require('../../Common/sources/mailService');
|
||||
const operationContext = require('../../Common/sources/operationContext');
|
||||
|
||||
const ctx = new operationContext.Context();
|
||||
const defaultTestSMTPServer = {
|
||||
host: 'smtp.ethereal.email',
|
||||
port: 587
|
||||
};
|
||||
const testTimeout = 1000 * 10;
|
||||
|
||||
afterAll(function () {
|
||||
mailService.transportersRelease();
|
||||
})
|
||||
|
||||
describe('Mail service', function () {
|
||||
console.log('!!!!!!!!!!!!!!!!', global.dev);
|
||||
describe('SMTP', function () {
|
||||
const { host, port } = defaultTestSMTPServer;
|
||||
|
||||
@ -23,7 +22,7 @@ describe('Mail service', function () {
|
||||
// Ethereial is a special SMTP sever for mailing tests in collaboration with Nodemailer.
|
||||
const accounts = await Promise.all([nodemailer.createTestAccount(), nodemailer.createTestAccount(), nodemailer.createTestAccount()]);
|
||||
const auth = accounts.map(account => { return { user: account.user, pass: account.pass }});
|
||||
auth.forEach(credential => mailService.createTransporter(ctx, host, port, credential, { from: 'some.mail@ethereal.com' }));
|
||||
auth.forEach(credential => mailService.createTransporter(host, port, credential, { from: 'some.mail@ethereal.com' }));
|
||||
|
||||
for (let i = 0; i < auth.length; i++) {
|
||||
const credentials = auth[i];
|
||||
@ -37,7 +36,7 @@ describe('Mail service', function () {
|
||||
}
|
||||
|
||||
const accountToBeDeleted = auth[1];
|
||||
mailService.deleteTransporter(ctx, host, accountToBeDeleted.user);
|
||||
mailService.deleteTransporter(host, accountToBeDeleted.user);
|
||||
|
||||
const errorPromise = mailService.send(
|
||||
host,
|
||||
@ -46,6 +45,6 @@ describe('Mail service', function () {
|
||||
);
|
||||
|
||||
await expect(errorPromise).rejects.toEqual(`MailService: no transporter exists for host "${host}" and user "${accountToBeDeleted.user}"`);
|
||||
});
|
||||
}, testTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user