mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-04-07 14:04:35 +08:00
[feature] Notification service added
This commit is contained in:
@ -16,13 +16,38 @@
|
|||||||
"visibilityTimeout": 300,
|
"visibilityTimeout": 300,
|
||||||
"retentionPeriod": 900
|
"retentionPeriod": 900
|
||||||
},
|
},
|
||||||
"mail": {
|
"email": {
|
||||||
"smtpConnectionConfiguration": {
|
"smtpServerConfiguration": {
|
||||||
|
"host": "smtp.ethereal.email",
|
||||||
|
"port": 587,
|
||||||
|
"auth": {
|
||||||
|
"user": "onlyoffice",
|
||||||
|
"pass": "onlyoffice"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"messageDefaults": {
|
"connectionConfiguration": {
|
||||||
"from": "some.mail@server.com",
|
|
||||||
"disableFileAccess": false,
|
"disableFileAccess": false,
|
||||||
"disableUrlAccess": 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": {
|
"storage": {
|
||||||
|
|||||||
13
Common/npm-shrinkwrap.json
generated
13
Common/npm-shrinkwrap.json
generated
@ -915,6 +915,13 @@
|
|||||||
"@smithy/util-retry": "^1.0.4",
|
"@smithy/util-retry": "^1.0.4",
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
"uuid": "^8.3.2"
|
"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": {
|
"@smithy/middleware-serde": {
|
||||||
@ -1980,9 +1987,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "8.3.2",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
|
||||||
},
|
},
|
||||||
"verror": {
|
"verror": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"request-filtering-agent": "1.0.5",
|
"request-filtering-agent": "1.0.5",
|
||||||
"rhea": "1.0.24",
|
"rhea": "1.0.24",
|
||||||
"uri-js": "4.2.2",
|
"uri-js": "4.2.2",
|
||||||
|
"uuid": "9.0.1",
|
||||||
"win-ca": "3.5.0"
|
"win-ca": "3.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,9 +35,11 @@
|
|||||||
const config = require('config');
|
const config = require('config');
|
||||||
const nodemailer = require('nodemailer');
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
const cfgConnection = config.get('mail.smtpConnectionConfiguration');
|
const operationContext = require('./operationContext');
|
||||||
const cfgMessageDefaults = config.get('mail.messageDefaults');
|
|
||||||
|
|
||||||
|
const cfgConnection = config.get('email.connectionConfiguration');
|
||||||
|
|
||||||
|
const ctx = new operationContext.Context();
|
||||||
const connectionDefaultSettings = {
|
const connectionDefaultSettings = {
|
||||||
pool: true,
|
pool: true,
|
||||||
socketTimeout: 1000 * 60 * 2,
|
socketTimeout: 1000 * 60 * 2,
|
||||||
@ -48,7 +50,7 @@ const connectionDefaultSettings = {
|
|||||||
const settings = Object.assign(connectionDefaultSettings, cfgConnection);
|
const settings = Object.assign(connectionDefaultSettings, cfgConnection);
|
||||||
const smtpTransporters = new Map();
|
const smtpTransporters = new Map();
|
||||||
|
|
||||||
function createTransporter(ctx, host, port, auth, messageCommonParameters = {}) {
|
function createTransporter(host, port, auth, messageCommonParameters = {}) {
|
||||||
const server = {
|
const server = {
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
@ -56,10 +58,13 @@ function createTransporter(ctx, host, port, auth, messageCommonParameters = {})
|
|||||||
secure: port === 465
|
secure: port === 465
|
||||||
};
|
};
|
||||||
const transport = Object.assign({}, server, settings);
|
const transport = Object.assign({}, server, settings);
|
||||||
const mailDefaults = Object.assign({}, cfgMessageDefaults, messageCommonParameters);
|
|
||||||
|
|
||||||
try {
|
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);
|
smtpTransporters.set(`${host}:${auth.user}`, transporter);
|
||||||
} catch (error) {
|
} 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);
|
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);
|
return transporter.sendMail(mailObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTransporter(ctx, host, userLogin) {
|
function deleteTransporter(host, userLogin) {
|
||||||
const transporter = smtpTransporters.get(`${host}:${userLogin}`);
|
const transporter = smtpTransporters.get(`${host}:${userLogin}`);
|
||||||
if (!transporter) {
|
if (!transporter) {
|
||||||
ctx.logger.error(`MailService: no transporter exists for host "${host}" and user "${userLogin}"`);
|
ctx.logger.error(`MailService: no transporter exists for host "${host}" and user "${userLogin}"`);
|
||||||
@ -91,6 +96,10 @@ function transportersRelease() {
|
|||||||
smtpTransporters.clear();
|
smtpTransporters.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCreated(host, user) {
|
||||||
|
return smtpTransporters
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createTransporter,
|
createTransporter,
|
||||||
send,
|
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 nodemailer = require('../../Common/node_modules/nodemailer');
|
||||||
|
|
||||||
const mailService = require('../../Common/sources/mailService');
|
const mailService = require('../../Common/sources/mailService');
|
||||||
const operationContext = require('../../Common/sources/operationContext');
|
|
||||||
|
|
||||||
const ctx = new operationContext.Context();
|
|
||||||
const defaultTestSMTPServer = {
|
const defaultTestSMTPServer = {
|
||||||
host: 'smtp.ethereal.email',
|
host: 'smtp.ethereal.email',
|
||||||
port: 587
|
port: 587
|
||||||
};
|
};
|
||||||
|
const testTimeout = 1000 * 10;
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(function () {
|
||||||
mailService.transportersRelease();
|
mailService.transportersRelease();
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Mail service', function () {
|
describe('Mail service', function () {
|
||||||
|
console.log('!!!!!!!!!!!!!!!!', global.dev);
|
||||||
describe('SMTP', function () {
|
describe('SMTP', function () {
|
||||||
const { host, port } = defaultTestSMTPServer;
|
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.
|
// 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 accounts = await Promise.all([nodemailer.createTestAccount(), nodemailer.createTestAccount(), nodemailer.createTestAccount()]);
|
||||||
const auth = accounts.map(account => { return { user: account.user, pass: account.pass }});
|
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++) {
|
for (let i = 0; i < auth.length; i++) {
|
||||||
const credentials = auth[i];
|
const credentials = auth[i];
|
||||||
@ -37,7 +36,7 @@ describe('Mail service', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accountToBeDeleted = auth[1];
|
const accountToBeDeleted = auth[1];
|
||||||
mailService.deleteTransporter(ctx, host, accountToBeDeleted.user);
|
mailService.deleteTransporter(host, accountToBeDeleted.user);
|
||||||
|
|
||||||
const errorPromise = mailService.send(
|
const errorPromise = mailService.send(
|
||||||
host,
|
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}"`);
|
await expect(errorPromise).rejects.toEqual(`MailService: no transporter exists for host "${host}" and user "${accountToBeDeleted.user}"`);
|
||||||
});
|
}, testTimeout);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user