Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
8997ded81d | |||
f177d8e9ab | |||
808a9cc856 | |||
be1c8d1164 | |||
2ecb2f3aa0 | |||
01dcdebda5 | |||
2adcc249de | |||
543e696bfc | |||
796e0204ca | |||
f5a36ab53a |
33
package.json
33
package.json
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/platformservice",
|
"name": "@serve.zone/platformservice",
|
||||||
"version": "1.0.2",
|
"private": true,
|
||||||
|
"version": "1.0.7",
|
||||||
"description": "contains the platformservice container with mail, sms, letter, ai services.",
|
"description": "contains the platformservice container with mail, sms, letter, ai services.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
@ -9,24 +10,40 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/)",
|
||||||
"start": "(node --max_old_space_size=100 ./cli.js)",
|
"start": "(node --max_old_space_size=250 ./cli.js)",
|
||||||
"startTs": "(node cli.ts.js)",
|
"startTs": "(node cli.ts.js)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)"
|
"localPublish": ""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.17",
|
"@git.zone/tsbuild": "^2.1.17",
|
||||||
"@git.zone/tsrun": "^1.2.8",
|
"@git.zone/tsrun": "^1.2.8",
|
||||||
"@git.zone/tstest": "^1.0.28",
|
"@git.zone/tstest": "^1.0.88",
|
||||||
"@git.zone/tswatch": "^2.0.1",
|
"@git.zone/tswatch": "^2.0.1",
|
||||||
"@push.rocks/tapbundle": "^5.0.3"
|
"@push.rocks/tapbundle": "^5.0.17"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.0.4",
|
"@anthropic-ai/sdk": "^0.18.0",
|
||||||
"@api.global/typedserver": "^3.0.20",
|
"@api.global/typedrequest": "^3.0.19",
|
||||||
|
"@api.global/typedserver": "^3.0.27",
|
||||||
|
"@api.global/typedsocket": "^3.0.0",
|
||||||
|
"@apiclient.xyz/cloudflare": "^6.0.3",
|
||||||
|
"@apiclient.xyz/letterxpress": "^1.0.17",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@push.rocks/qenv": "^6.0.5",
|
"@push.rocks/qenv": "^6.0.5",
|
||||||
"@push.rocks/smartdata": "^5.0.7",
|
"@push.rocks/smartdata": "^5.0.7",
|
||||||
|
"@push.rocks/smartfile": "^11.0.4",
|
||||||
|
"@push.rocks/smartlog": "^3.0.3",
|
||||||
|
"@push.rocks/smartmail": "^1.0.24",
|
||||||
"@push.rocks/smartpath": "^5.0.5",
|
"@push.rocks/smartpath": "^5.0.5",
|
||||||
"@push.rocks/smartstate": "^2.0.0"
|
"@push.rocks/smartpromise": "^4.0.3",
|
||||||
|
"@push.rocks/smartrequest": "^2.0.21",
|
||||||
|
"@push.rocks/smartrx": "^3.0.7",
|
||||||
|
"@push.rocks/smartstate": "^2.0.0",
|
||||||
|
"@serve.zone/interfaces": "^1.0.47",
|
||||||
|
"@tsclass/tsclass": "^4.0.52",
|
||||||
|
"mailauth": "^4.6.5",
|
||||||
|
"mailparser": "^3.6.9",
|
||||||
|
"openai": "^4.29.1",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4012
pnpm-lock.yaml
generated
4012
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
5
test/test.ts
Normal file
5
test/test.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
|
||||||
|
tap.test('should create a platform service', async () => {});
|
||||||
|
|
||||||
|
tap.start();
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/platformservice',
|
name: '@serve.zone/platformservice',
|
||||||
version: '1.0.2',
|
version: '1.0.7',
|
||||||
description: 'contains the platformservice container with mail, sms, letter, ai services.'
|
description: 'contains the platformservice container with mail, sms, letter, ai services.'
|
||||||
}
|
}
|
||||||
|
50
ts/aibridge/aibridge.classes.aibridge.ts
Normal file
50
ts/aibridge/aibridge.classes.aibridge.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import * as plugins from './aibridge.plugins.js';
|
||||||
|
import * as paths from './aibridge.paths.js';
|
||||||
|
import { AiBridgeDb } from './aibridge.classes.aibridgedb.js';
|
||||||
|
import { OpenAiBridge } from './aibridge.classes.openaibridge.js';
|
||||||
|
|
||||||
|
export class AiBridge {
|
||||||
|
public projectinfo: plugins.projectinfo.ProjectInfo;
|
||||||
|
public serverInstance: plugins.loleServiceserver.ServiceServer;
|
||||||
|
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
||||||
|
public aibridgeDb: AiBridgeDb;
|
||||||
|
|
||||||
|
public openAiBridge: OpenAiBridge;
|
||||||
|
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
this.aibridgeDb = new AiBridgeDb(this);
|
||||||
|
await this.aibridgeDb.start();
|
||||||
|
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
this.openAiBridge = new OpenAiBridge(this);
|
||||||
|
await this.openAiBridge.start();
|
||||||
|
|
||||||
|
// server
|
||||||
|
this.serverInstance = new plugins.loleServiceserver.ServiceServer({
|
||||||
|
serviceDomain: 'aibridge.lossless.one',
|
||||||
|
serviceName: 'aibridge',
|
||||||
|
serviceVersion: this.projectinfo.npm.version,
|
||||||
|
addCustomRoutes: async (serverArg) => {
|
||||||
|
// any custom route configs go here
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// lets implemenet the actual typedrequest functions
|
||||||
|
this.typedrouter.addTypedHandler<plugins.lointAiBridge.requests.IReq_Chat>(new plugins.typedrequest.TypedHandler('chat', async reqArg => {
|
||||||
|
const resultChat = await this.openAiBridge.chat(reqArg.chat.systemMessage, reqArg.chat.messages[reqArg.chat.messages.length - 1].content, reqArg.chat.messages);
|
||||||
|
return {
|
||||||
|
chat: reqArg.chat,
|
||||||
|
latestMessage: resultChat.message.content,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
await this.serverInstance.start();
|
||||||
|
this.serverInstance.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
await this.serverInstance.stop();
|
||||||
|
await this.aibridgeDb.stop();
|
||||||
|
}
|
||||||
|
}
|
25
ts/aibridge/aibridge.classes.aibridgedb.ts
Normal file
25
ts/aibridge/aibridge.classes.aibridgedb.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as plugins from './aibridge.plugins.js';
|
||||||
|
import { AiBridge } from './aibridge.classes.aibridge.js';
|
||||||
|
|
||||||
|
export class AiBridgeDb {
|
||||||
|
public smartdataDb: plugins.smartdata.SmartdataDb;
|
||||||
|
public aibridgeRef: AiBridge;
|
||||||
|
|
||||||
|
constructor(aibridgeRefArg: AiBridge) {
|
||||||
|
this.aibridgeRef = aibridgeRefArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
this.smartdataDb = new plugins.smartdata.SmartdataDb({
|
||||||
|
mongoDbUser: await this.aibridgeRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_USER'),
|
||||||
|
mongoDbName: await this.aibridgeRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_NAME'),
|
||||||
|
mongoDbPass: await this.aibridgeRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_PASS'),
|
||||||
|
mongoDbUrl: await this.aibridgeRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_URL'),
|
||||||
|
});
|
||||||
|
await this.smartdataDb.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
await this.smartdataDb.close();
|
||||||
|
}
|
||||||
|
}
|
58
ts/aibridge/aibridge.classes.openaibridge.ts
Normal file
58
ts/aibridge/aibridge.classes.openaibridge.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { AiBridge } from './aibridge.classes.aibridge.js';
|
||||||
|
import * as plugins from './aibridge.plugins.js';
|
||||||
|
import * as paths from './aibridge.paths.js';
|
||||||
|
|
||||||
|
export class OpenAiBridge {
|
||||||
|
public aiBridgeRef: AiBridge;
|
||||||
|
public openAiApiClient: plugins.openai.default;
|
||||||
|
constructor(aiBridgeRefArg: AiBridge) {
|
||||||
|
this.aiBridgeRef = aiBridgeRefArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
const openAiToken = await this.aiBridgeRef.serviceQenv.getEnvVarOnDemand('OPENAI_TOKEN');
|
||||||
|
this.openAiApiClient = new plugins.openai.default({
|
||||||
|
apiKey: openAiToken,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {}
|
||||||
|
|
||||||
|
public async chat(
|
||||||
|
systemMessage: string,
|
||||||
|
userMessage: string,
|
||||||
|
messageHistory: {
|
||||||
|
role: 'assistant' | 'user';
|
||||||
|
content: string;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
const result = await this.openAiApiClient.chat.completions.create({
|
||||||
|
model: 'gpt-3.5-turbo-16k-0613',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemMessage },
|
||||||
|
...messageHistory,
|
||||||
|
{ role: 'user', content: userMessage },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: result.choices[0].message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async audio(messageArg: string) {
|
||||||
|
const done = plugins.smartpromise.defer();
|
||||||
|
const result = await this.openAiApiClient.audio.speech.create({
|
||||||
|
model: 'tts-1-hd',
|
||||||
|
input: messageArg,
|
||||||
|
voice: 'nova',
|
||||||
|
response_format: 'mp3',
|
||||||
|
speed: 1,
|
||||||
|
});
|
||||||
|
const stream = result.body.pipe(plugins.smartfile.fsStream.createWriteStream(plugins.path.join(paths.nogitDir, 'output.mp3')));
|
||||||
|
stream.on('finish', () => {
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
return done.promise;
|
||||||
|
}
|
||||||
|
}
|
16
ts/aibridge/aibridge.paths.ts
Normal file
16
ts/aibridge/aibridge.paths.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as plugins from './aibridge.plugins.js';
|
||||||
|
|
||||||
|
export const packageDir = plugins.path.join(
|
||||||
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
|
'../'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const assetsDir = plugins.path.join(
|
||||||
|
packageDir,
|
||||||
|
'./assets/'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const nogitDir = plugins.path.join(
|
||||||
|
packageDir,
|
||||||
|
'./.nogit/'
|
||||||
|
);
|
32
ts/aibridge/aibridge.plugins.ts
Normal file
32
ts/aibridge/aibridge.plugins.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// node native
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export { path };
|
||||||
|
|
||||||
|
// @losslessone_private scope
|
||||||
|
import * as loleServiceserver from '@losslessone_private/lole-serviceserver';
|
||||||
|
import * as lointAiBridge from '@losslessone_private/loint-aibridge';
|
||||||
|
|
||||||
|
export { loleServiceserver, lointAiBridge };
|
||||||
|
|
||||||
|
// apiglobal scope
|
||||||
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
|
|
||||||
|
export {
|
||||||
|
typedrequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushrocks scope
|
||||||
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
|
import * as qenv from '@push.rocks/qenv';
|
||||||
|
import * as smartdata from '@push.rocks/smartdata';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
|
||||||
|
export { projectinfo, qenv, smartdata, smartfile, smartpath, smartpromise };
|
||||||
|
|
||||||
|
// thirdparty scope
|
||||||
|
import * as antrophic from '@anthropic-ai/sdk';
|
||||||
|
import * as openai from 'openai';
|
||||||
|
export { antrophic as anthropic, openai };
|
17
ts/aibridge/index.ts
Normal file
17
ts/aibridge/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { AiBridge } from './aibridge.classes.aibridge.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
AiBridge,
|
||||||
|
}
|
||||||
|
|
||||||
|
let aibridgeInstance: AiBridge;
|
||||||
|
export const runCli = async () => {
|
||||||
|
aibridgeInstance = new AiBridge();
|
||||||
|
await aibridgeInstance.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stop = async () => {
|
||||||
|
if (aibridgeInstance) {
|
||||||
|
await aibridgeInstance.stop();
|
||||||
|
}
|
||||||
|
};
|
@ -1,6 +1,10 @@
|
|||||||
import * as plugins from './platformservice.plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from './platformservice.paths.js';
|
import * as paths from './paths.js';
|
||||||
import { PlatformServiceDb } from './classes.platformservicedb.js'
|
import { PlatformServiceDb } from './classes.platformservicedb.js'
|
||||||
|
import { EmailService } from './email/email.classes.emailservice.js';
|
||||||
|
import { SmsService } from './sms/smsservice.js';
|
||||||
|
import { LetterService } from './letter/classes.letterservice.js';
|
||||||
|
import { MtaService } from './mta/mta.classes.mta.js';
|
||||||
|
|
||||||
export class SzPlatformService {
|
export class SzPlatformService {
|
||||||
public projectinfo: plugins.projectinfo.ProjectInfo;
|
public projectinfo: plugins.projectinfo.ProjectInfo;
|
||||||
@ -10,9 +14,28 @@ export class SzPlatformService {
|
|||||||
public typedserver: plugins.typedserver.TypedServer;
|
public typedserver: plugins.typedserver.TypedServer;
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
// SubServices
|
||||||
|
public emailService: EmailService;
|
||||||
|
public letterService: LetterService;
|
||||||
|
public mtaService: MtaService;
|
||||||
|
public smsService: SmsService;
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
this.platformserviceDb = new PlatformServiceDb(this);
|
this.platformserviceDb = new PlatformServiceDb(this);
|
||||||
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
|
||||||
|
// lets start the sub services
|
||||||
|
this.emailService = new EmailService(this);
|
||||||
|
this.letterService = new LetterService(this, {
|
||||||
|
letterxpressUser: await this.serviceQenv.getEnvVarOnDemand('LETTER_API_USER'),
|
||||||
|
letterxpressToken: await this.serviceQenv.getEnvVarOnDemand('LETTER_API_TOKEN')
|
||||||
|
});
|
||||||
|
this.mtaService = new MtaService(this);
|
||||||
|
this.smsService = new SmsService(this, {
|
||||||
|
apiGatewayApiToken: await this.serviceQenv.getEnvVarOnDemand('SMS_API_TOKEN'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// lets start the server finally
|
||||||
this.typedserver = new plugins.typedserver.TypedServer({
|
this.typedserver = new plugins.typedserver.TypedServer({
|
||||||
cors: true,
|
cors: true,
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as plugins from './platformservice.plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { SzPlatformService } from './classes.platformservice.js';
|
import { SzPlatformService } from './classes.platformservice.js';
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import * as plugins from './email.plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { Email } from './email.classes.email.js';
|
import { EmailService } from './email.classes.emailservice.js';
|
||||||
import { request } from 'http';
|
import { logger } from '../logger.js';
|
||||||
import { logger } from './email.logging.js';
|
|
||||||
|
|
||||||
export class ApiManager {
|
export class ApiManager {
|
||||||
public emailRef: Email;
|
public emailRef: EmailService;
|
||||||
|
|
||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
constructor(emailRefArg: Email) {
|
constructor(emailRefArg: EmailService) {
|
||||||
this.emailRef = emailRefArg;
|
this.emailRef = emailRefArg;
|
||||||
this.emailRef.mainTypedRouter.addTypedRouter(this.typedRouter);
|
this.emailRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||||
this.typedRouter.addTypedHandler<plugins.lointEmail.IRequestSendEmail>(
|
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.platformservice.mta.IRequest_SendEmail>(
|
||||||
new plugins.typedrequest.TypedHandler('sendEmail', async (requestData) => {
|
new plugins.typedrequest.TypedHandler('sendEmail', async (requestData) => {
|
||||||
const mailToSend = new plugins.smartmail.Smartmail({
|
const mailToSend = new plugins.smartmail.Smartmail({
|
||||||
body: requestData.body,
|
body: requestData.body,
|
||||||
@ -22,7 +21,7 @@ export class ApiManager {
|
|||||||
if (requestData.attachments) {
|
if (requestData.attachments) {
|
||||||
for (const attachment of requestData.attachments) {
|
for (const attachment of requestData.attachments) {
|
||||||
mailToSend.addAttachment(
|
mailToSend.addAttachment(
|
||||||
await plugins.smartfile.Smartfile.fromString(
|
await plugins.smartfile.SmartFile.fromString(
|
||||||
attachment.name,
|
attachment.name,
|
||||||
attachment.binaryAttachmentString,
|
attachment.binaryAttachmentString,
|
||||||
'binary'
|
'binary'
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import * as plugins from './email.plugins.js';
|
import * as plugins from './email.plugins.js';
|
||||||
import { Email } from './email.classes.email.js';
|
import { EmailService } from './email.classes.emailservice.js';
|
||||||
|
|
||||||
export class MailgunConnector {
|
export class MailgunConnector {
|
||||||
public emailRef: Email;
|
public emailRef: EmailService;
|
||||||
public mailgunAccount: plugins.mailgun.MailgunAccount;
|
public mailgunAccount: plugins.mailgun.MailgunAccount;
|
||||||
|
|
||||||
constructor(emailRefArg: Email) {
|
constructor(emailRefArg: EmailService) {
|
||||||
this.emailRef = emailRefArg;
|
this.emailRef = emailRefArg;
|
||||||
this.mailgunAccount = new plugins.mailgun.MailgunAccount({
|
this.mailgunAccount = new plugins.mailgun.MailgunAccount({
|
||||||
apiToken: this.emailRef.qenv.getEnvVarOnDemand('MAILGUN_API_TOKEN'),
|
apiToken: this.emailRef.qenv.getEnvVarOnDemand('MAILGUN_API_TOKEN'),
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import * as plugins from './email.plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from './email.paths.js';
|
import * as paths from '../paths.js';
|
||||||
import { MailgunConnector } from './email.classes.connector.mailgun.js';
|
import { MailgunConnector } from './email.classes.connector.mailgun.js';
|
||||||
import { RuleManager } from './email.classes.rulemanager.js';
|
import { RuleManager } from './email.classes.rulemanager.js';
|
||||||
import { ApiManager } from './email.classes.apimanager.js';
|
import { ApiManager } from './email.classes.apimanager.js';
|
||||||
import { logger } from './email.logging.js';
|
import { logger } from '../logger.js';
|
||||||
import type { SzPlatformService } from '../classes.platformservice.js';
|
import type { SzPlatformService } from '../classes.platformservice.js';
|
||||||
|
|
||||||
export class Email {
|
|
||||||
|
export interface IEmailConstructorOptions {
|
||||||
|
mailgunApiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailService {
|
||||||
public platformServiceRef: SzPlatformService;
|
public platformServiceRef: SzPlatformService;
|
||||||
|
|
||||||
// typedrouter
|
// typedrouter
|
||||||
public mainTypedRouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
// connectors
|
// connectors
|
||||||
public mailgunConnector: MailgunConnector;
|
public mailgunConnector: MailgunConnector;
|
||||||
@ -22,11 +27,12 @@ export class Email {
|
|||||||
|
|
||||||
constructor(platformServiceRefArg: SzPlatformService) {
|
constructor(platformServiceRefArg: SzPlatformService) {
|
||||||
this.platformServiceRef = platformServiceRefArg;
|
this.platformServiceRef = platformServiceRefArg;
|
||||||
|
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
this.mailgunConnector = new MailgunConnector(this);
|
this.mailgunConnector = new MailgunConnector(this);
|
||||||
this.ruleManager = new RuleManager(this);
|
this.ruleManager = new RuleManager(this);
|
||||||
this.platformServiceRef.typedserver.server.addRoute(
|
this.platformServiceRef.typedserver.server.addRoute(
|
||||||
'/mailgun-notify',
|
'/mailgun-notify',
|
||||||
new plugins.loleServiceserver.Handler('POST', async (req, res) => {
|
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
|
||||||
console.log('Got a mailgun email notification');
|
console.log('Got a mailgun email notification');
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.end();
|
res.end();
|
@ -1,14 +1,14 @@
|
|||||||
import * as plugins from './email.plugins.js';
|
import * as plugins from './email.plugins.js';
|
||||||
import { Email } from './email.classes.email.js';
|
import { EmailService } from './email.classes.emailservice.js';
|
||||||
import { logger } from './email.logging.js';
|
import { logger } from './email.logging.js';
|
||||||
|
|
||||||
export class RuleManager {
|
export class RuleManager {
|
||||||
public emailRef: Email;
|
public emailRef: EmailService;
|
||||||
public smartruleInstance = new plugins.smartrule.SmartRule<
|
public smartruleInstance = new plugins.smartrule.SmartRule<
|
||||||
plugins.smartmail.Smartmail<plugins.mailgun.IMailgunMessage>
|
plugins.smartmail.Smartmail<plugins.mailgun.IMailgunMessage>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(emailRefArg: Email) {
|
constructor(emailRefArg: EmailService) {
|
||||||
this.emailRef = emailRefArg;
|
this.emailRef = emailRefArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import * as plugins from './email.plugins.js';
|
|
||||||
import * as paths from './email.paths.js';
|
|
||||||
|
|
||||||
const projectInfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
|
||||||
|
|
||||||
export const logger = plugins.loleLog.createLoleLogger({
|
|
||||||
companyUnit: 'lossless.cloud',
|
|
||||||
containerName: 'email',
|
|
||||||
containerVersion: projectInfoNpm.version,
|
|
||||||
sentryAppName: 'email',
|
|
||||||
sentryDsn: 'https://7037e86f36134ced85ae56a57daa1e5e@o169278.ingest.sentry.io/5280282',
|
|
||||||
zone: 'servezone',
|
|
||||||
});
|
|
@ -1,6 +0,0 @@
|
|||||||
import * as plugins from './email.plugins.js';
|
|
||||||
|
|
||||||
export const packageDir = plugins.path.join(
|
|
||||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
|
||||||
'../'
|
|
||||||
);
|
|
@ -1,43 +0,0 @@
|
|||||||
// native scope
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
export { path };
|
|
||||||
|
|
||||||
// @losslessone_private scope
|
|
||||||
import * as loleLog from '@losslessone_private/lole-log';
|
|
||||||
import * as loleServiceserver from '@losslessone_private/lole-serviceserver';
|
|
||||||
|
|
||||||
import * as lointEmail from '@losslessone_private/loint-email';
|
|
||||||
|
|
||||||
export { loleLog, loleServiceserver, lointEmail };
|
|
||||||
|
|
||||||
// @apiglobal scope
|
|
||||||
import * as typedrequest from '@apiglobal/typedrequest';
|
|
||||||
|
|
||||||
export { typedrequest };
|
|
||||||
|
|
||||||
// @mojoio scope
|
|
||||||
import * as mailgun from '@mojoio/mailgun';
|
|
||||||
|
|
||||||
export { mailgun };
|
|
||||||
|
|
||||||
// @pushrocks scope
|
|
||||||
import * as projectinfo from '@pushrocks/projectinfo';
|
|
||||||
import * as qenv from '@pushrocks/qenv';
|
|
||||||
import * as smartfile from '@pushrocks/smartfile';
|
|
||||||
import * as smartmail from '@pushrocks/smartmail';
|
|
||||||
import * as smartpath from '@pushrocks/smartpath';
|
|
||||||
import * as smartrequest from '@pushrocks/smartrequest';
|
|
||||||
import * as smartrule from '@pushrocks/smartrule';
|
|
||||||
import * as smartvalidator from '@pushrocks/smartvalidator';
|
|
||||||
|
|
||||||
export {
|
|
||||||
projectinfo,
|
|
||||||
qenv,
|
|
||||||
smartfile,
|
|
||||||
smartmail,
|
|
||||||
smartpath,
|
|
||||||
smartrequest,
|
|
||||||
smartrule,
|
|
||||||
smartvalidator,
|
|
||||||
};
|
|
@ -1,3 +1,3 @@
|
|||||||
import { Email } from './email.classes.email.js';
|
import { EmailService } from './email.classes.emailservice.js';
|
||||||
|
|
||||||
export { Email };
|
export { EmailService as Email };
|
@ -1,4 +1,4 @@
|
|||||||
export * from './00_commitinfo_data.js';
|
export * from './00_commitinfo_data.js';
|
||||||
import { SzPlatformService } from './classes.platformservice.js'
|
import { SzPlatformService } from './classes.platformservice.js';
|
||||||
|
|
||||||
export const runCli = async () => {}
|
export const runCli = async () => {}
|
41
ts/letter/classes.letterservice.ts
Normal file
41
ts/letter/classes.letterservice.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { SzPlatformService } from '../classes.platformservice.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface ILetterConstructorOptions {
|
||||||
|
letterxpressUser: string;
|
||||||
|
letterxpressToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LetterService {
|
||||||
|
public platformServiceRef: SzPlatformService;
|
||||||
|
public options: ILetterConstructorOptions;
|
||||||
|
public letterxpressAccount: plugins.letterxpress.LetterXpressAccount;
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(platformServiceRefArg: SzPlatformService, optionsArg: ILetterConstructorOptions) {
|
||||||
|
this.platformServiceRef = platformServiceRefArg;
|
||||||
|
this.options = optionsArg;
|
||||||
|
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<
|
||||||
|
plugins.servezoneInterfaces.platformservice.letter.IRequest_SendLetter
|
||||||
|
>(new plugins.typedrequest.TypedHandler('sendLetter', async dataArg => {
|
||||||
|
if(dataArg.needsCover) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
processId: '',
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
this.letterxpressAccount = new plugins.letterxpress.LetterXpressAccount({
|
||||||
|
username: this.options.letterxpressUser,
|
||||||
|
apiKey: this.options.letterxpressToken,
|
||||||
|
});
|
||||||
|
await this.letterxpressAccount.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {}
|
||||||
|
}
|
0
ts/letter/index.ts
Normal file
0
ts/letter/index.ts
Normal file
9
ts/logger.ts
Normal file
9
ts/logger.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
export const logger = new plugins.smartlog.Smartlog({
|
||||||
|
logContext: {
|
||||||
|
environment: 'production',
|
||||||
|
runtime: 'node',
|
||||||
|
zone: 'serve.zone',
|
||||||
|
}
|
||||||
|
});
|
8
ts/mta/index.ts
Normal file
8
ts/mta/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export * from './mta.classes.dkimcreator.js';
|
||||||
|
export * from './mta.classes.emailsignjob.js';
|
||||||
|
export * from './mta.classes.dkimverifier.js';
|
||||||
|
export * from './mta.classes.mta.js';
|
||||||
|
export * from './mta.classes.smtpserver.js';
|
||||||
|
export * from './mta.classes.emailsendjob.js';
|
||||||
|
export * from './mta.classes.mta.js';
|
||||||
|
export * from './mta.classes.email.js';
|
7
ts/mta/mta.classes.apimanager.ts
Normal file
7
ts/mta/mta.classes.apimanager.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export class ApiManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
119
ts/mta/mta.classes.dkimcreator.ts
Normal file
119
ts/mta/mta.classes.dkimcreator.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
|
||||||
|
import { Email } from './mta.classes.email.js';
|
||||||
|
import type { MtaService } from './mta.classes.mta.js';
|
||||||
|
|
||||||
|
const readFile = plugins.util.promisify(plugins.fs.readFile);
|
||||||
|
const writeFile = plugins.util.promisify(plugins.fs.writeFile);
|
||||||
|
const generateKeyPair = plugins.util.promisify(plugins.crypto.generateKeyPair);
|
||||||
|
|
||||||
|
export interface IKeyPaths {
|
||||||
|
privateKeyPath: string;
|
||||||
|
publicKeyPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DKIMCreator {
|
||||||
|
private keysDir: string;
|
||||||
|
|
||||||
|
constructor(metaRef: MtaService, keysDir = paths.keysDir) {
|
||||||
|
this.keysDir = keysDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> {
|
||||||
|
return {
|
||||||
|
privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`),
|
||||||
|
publicKeyPath: plugins.path.join(this.keysDir, `${domainArg}-public.pem`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a DKIM key is present and creates one and stores it to disk otherwise
|
||||||
|
public async handleDKIMKeysForDomain(domainArg: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.readDKIMKeys(domainArg);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`No DKIM keys found for ${domainArg}. Generating...`);
|
||||||
|
await this.createAndStoreDKIMKeys(domainArg);
|
||||||
|
const dnsValue = await this.getDNSRecordForDomain(domainArg);
|
||||||
|
plugins.smartfile.fs.ensureDirSync(paths.dnsRecordsDir);
|
||||||
|
plugins.smartfile.memory.toFsSync(JSON.stringify(dnsValue, null, 2), plugins.path.join(paths.dnsRecordsDir, `${domainArg}.dkimrecord.json`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleDKIMKeysForEmail(email: Email): Promise<void> {
|
||||||
|
const domain = email.from.split('@')[1];
|
||||||
|
await this.handleDKIMKeysForDomain(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read DKIM keys from disk
|
||||||
|
public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> {
|
||||||
|
const keyPaths = await this.getKeyPathsForDomain(domainArg);
|
||||||
|
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
|
||||||
|
readFile(keyPaths.privateKeyPath),
|
||||||
|
readFile(keyPaths.publicKeyPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Convert the buffers to strings
|
||||||
|
const privateKey = privateKeyBuffer.toString();
|
||||||
|
const publicKey = publicKeyBuffer.toString();
|
||||||
|
|
||||||
|
return { privateKey, publicKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a DKIM key pair
|
||||||
|
private async createDKIMKeys(): Promise<{ privateKey: string; publicKey: string }> {
|
||||||
|
const { privateKey, publicKey } = await generateKeyPair('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||||
|
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { privateKey, publicKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store a DKIM key pair to disk
|
||||||
|
private async storeDKIMKeys(
|
||||||
|
privateKey: string,
|
||||||
|
publicKey: string,
|
||||||
|
privateKeyPath: string,
|
||||||
|
publicKeyPath: string
|
||||||
|
): Promise<void> {
|
||||||
|
await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a DKIM key pair and store it to disk
|
||||||
|
private async createAndStoreDKIMKeys(domain: string): Promise<void> {
|
||||||
|
const { privateKey, publicKey } = await this.createDKIMKeys();
|
||||||
|
const keyPaths = await this.getKeyPathsForDomain(domain);
|
||||||
|
await this.storeDKIMKeys(
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
keyPaths.privateKeyPath,
|
||||||
|
keyPaths.publicKeyPath
|
||||||
|
);
|
||||||
|
console.log(`DKIM keys for ${domain} created and stored.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDNSRecordForDomain(domainArg: string): Promise<plugins.tsclass.network.IDnsRecord> {
|
||||||
|
await this.handleDKIMKeysForDomain(domainArg);
|
||||||
|
const keys = await this.readDKIMKeys(domainArg);
|
||||||
|
|
||||||
|
// Remove the PEM header and footer and newlines
|
||||||
|
const pemHeader = '-----BEGIN PUBLIC KEY-----';
|
||||||
|
const pemFooter = '-----END PUBLIC KEY-----';
|
||||||
|
const keyContents = keys.publicKey
|
||||||
|
.replace(pemHeader, '')
|
||||||
|
.replace(pemFooter, '')
|
||||||
|
.replace(/\n/g, '');
|
||||||
|
|
||||||
|
// Now generate the DKIM DNS TXT record
|
||||||
|
const dnsRecordValue = `v=DKIM1; h=sha256; k=rsa; p=${keyContents}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `mta._domainkey.${domainArg}`,
|
||||||
|
type: 'TXT',
|
||||||
|
dnsSecEnabled: null,
|
||||||
|
value: dnsRecordValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
35
ts/mta/mta.classes.dkimverifier.ts
Normal file
35
ts/mta/mta.classes.dkimverifier.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { MtaService } from './mta.classes.mta.js';
|
||||||
|
|
||||||
|
class DKIMVerifier {
|
||||||
|
public mtaRef: MtaService;
|
||||||
|
|
||||||
|
constructor(mtaRefArg: MtaService) {
|
||||||
|
this.mtaRef = mtaRefArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verify(email: string): Promise<boolean> {
|
||||||
|
console.log('Trying to verify DKIM now...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verification = await plugins.mailauth.authenticate(email, {
|
||||||
|
/* resolver: (...args) => {
|
||||||
|
console.log(args);
|
||||||
|
} */
|
||||||
|
});
|
||||||
|
console.log(verification);
|
||||||
|
if (verification && verification.dkim.results[0].status.result === 'pass') {
|
||||||
|
console.log('DKIM Verification result: pass');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('DKIM Verification failed:', verification?.error || 'Unknown error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DKIM Verification failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DKIMVerifier };
|
10
ts/mta/mta.classes.dnsmanager.ts
Normal file
10
ts/mta/mta.classes.dnsmanager.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { MtaService } from './mta.classes.mta.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export class DNSManager {
|
||||||
|
public mtaRef: MtaService;
|
||||||
|
|
||||||
|
constructor(mtaRefArg: MtaService) {
|
||||||
|
this.mtaRef = mtaRefArg;
|
||||||
|
}
|
||||||
|
}
|
36
ts/mta/mta.classes.email.ts
Normal file
36
ts/mta/mta.classes.email.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export interface IAttachment {
|
||||||
|
filename: string;
|
||||||
|
content: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEmailOptions {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
subject: string;
|
||||||
|
text: string;
|
||||||
|
attachments: IAttachment[];
|
||||||
|
mightBeSpam?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Email {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
subject: string;
|
||||||
|
text: string;
|
||||||
|
attachments: IAttachment[];
|
||||||
|
mightBeSpam: boolean;
|
||||||
|
|
||||||
|
constructor(options: IEmailOptions) {
|
||||||
|
this.from = options.from;
|
||||||
|
this.to = options.to;
|
||||||
|
this.subject = options.subject;
|
||||||
|
this.text = options.text;
|
||||||
|
this.attachments = options.attachments;
|
||||||
|
this.mightBeSpam = options.mightBeSpam || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFromDomain() {
|
||||||
|
return this.from.split('@')[1]
|
||||||
|
}
|
||||||
|
}
|
173
ts/mta/mta.classes.emailsendjob.ts
Normal file
173
ts/mta/mta.classes.emailsendjob.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import { Email } from './mta.classes.email.js';
|
||||||
|
import { EmailSignJob } from './mta.classes.emailsignjob.js';
|
||||||
|
import type { MtaService } from './mta.classes.mta.js';
|
||||||
|
|
||||||
|
export class EmailSendJob {
|
||||||
|
mtaRef: MtaService;
|
||||||
|
private email: Email;
|
||||||
|
private socket: plugins.net.Socket | plugins.tls.TLSSocket = null;
|
||||||
|
private mxRecord: string = null;
|
||||||
|
|
||||||
|
constructor(mtaRef: MtaService, emailArg: Email) {
|
||||||
|
this.email = emailArg;
|
||||||
|
this.mtaRef = mtaRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(): Promise<void> {
|
||||||
|
const domain = this.email.to.split('@')[1];
|
||||||
|
const addresses = await this.resolveMx(domain);
|
||||||
|
addresses.sort((a, b) => a.priority - b.priority);
|
||||||
|
this.mxRecord = addresses[0].exchange;
|
||||||
|
|
||||||
|
console.log(`Using ${this.mxRecord} as mail server for domain ${domain}`);
|
||||||
|
|
||||||
|
this.socket = plugins.net.connect(25, this.mxRecord);
|
||||||
|
await this.processInitialResponse();
|
||||||
|
await this.sendCommand(`EHLO ${this.email.from.split('@')[1]}\r\n`, '250');
|
||||||
|
try {
|
||||||
|
await this.sendCommand('STARTTLS\r\n', '220');
|
||||||
|
this.socket = plugins.tls.connect({ socket: this.socket, rejectUnauthorized: false });
|
||||||
|
await this.processTLSUpgrade(this.email.from.split('@')[1]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error sending STARTTLS command:', error);
|
||||||
|
console.log('Continuing with unencrypted connection...');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveMx(domain: string): Promise<plugins.dns.MxRecord[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
plugins.dns.resolveMx(domain, (err, addresses) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error resolving MX:', err);
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(addresses);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processInitialResponse(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket.once('data', (data) => {
|
||||||
|
const response = data.toString();
|
||||||
|
if (!response.startsWith('220')) {
|
||||||
|
console.error('Unexpected initial server response:', response);
|
||||||
|
reject(new Error(`Unexpected initial server response: ${response}`));
|
||||||
|
} else {
|
||||||
|
console.log('Received initial server response:', response);
|
||||||
|
console.log('Connected to server, sending EHLO...');
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processTLSUpgrade(domain: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket.once('secureConnect', async () => {
|
||||||
|
console.log('TLS started successfully');
|
||||||
|
try {
|
||||||
|
await this.sendCommand(`EHLO ${domain}\r\n`, '250');
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error sending EHLO after TLS upgrade:', err);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendCommand(command: string, expectedResponseCode?: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket.write(command, (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedResponseCode) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.once('data', (data) => {
|
||||||
|
const response = data.toString();
|
||||||
|
if (response.startsWith('221')) {
|
||||||
|
this.socket.destroy();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
if (!response.startsWith(expectedResponseCode)) {
|
||||||
|
reject(new Error(`Unexpected server response: ${response}`));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendMessage(): Promise<void> {
|
||||||
|
console.log('Preparing email message...');
|
||||||
|
const messageId = `<${plugins.uuid.v4()}@${this.email.from.split('@')[1]}>`;
|
||||||
|
|
||||||
|
// Create a boundary for the email parts
|
||||||
|
const boundary = '----=_NextPart_' + plugins.uuid.v4();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
From: this.email.from,
|
||||||
|
To: this.email.to,
|
||||||
|
Subject: this.email.subject,
|
||||||
|
'Content-Type': `multipart/mixed; boundary="${boundary}"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct the body of the message
|
||||||
|
let body = `--${boundary}\r\nContent-Type: text/html; charset=utf-8\r\n\r\n${this.email.text}\r\n`;
|
||||||
|
|
||||||
|
// Then, the attachments
|
||||||
|
for (let attachment of this.email.attachments) {
|
||||||
|
body += `--${boundary}\r\nContent-Type: ${attachment.contentType}; name="${attachment.filename}"\r\n`;
|
||||||
|
body += 'Content-Transfer-Encoding: base64\r\n';
|
||||||
|
body += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n\r\n`;
|
||||||
|
body += attachment.content.toString('base64') + '\r\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of email
|
||||||
|
body += `--${boundary}--\r\n`;
|
||||||
|
|
||||||
|
// Create an instance of DKIMSigner
|
||||||
|
const dkimSigner = new EmailSignJob(this.mtaRef, {
|
||||||
|
domain: this.email.getFromDomain(), // Replace with your domain
|
||||||
|
selector: `mta`, // Replace with your DKIM selector
|
||||||
|
headers: headers,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct the message with DKIM-Signature header
|
||||||
|
let message = `Message-ID: ${messageId}\r\nFrom: ${this.email.from}\r\nTo: ${this.email.to}\r\nSubject: ${this.email.subject}\r\nContent-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`;
|
||||||
|
message += body;
|
||||||
|
|
||||||
|
let signatureHeader = await dkimSigner.getSignatureHeader(message);
|
||||||
|
message = `${signatureHeader}${message}`;
|
||||||
|
|
||||||
|
plugins.smartfile.fs.ensureDirSync(paths.sentEmailsDir);
|
||||||
|
plugins.smartfile.memory.toFsSync(message, plugins.path.join(paths.sentEmailsDir, `${Date.now()}.eml`));
|
||||||
|
|
||||||
|
|
||||||
|
// Adding necessary commands before sending the actual email message
|
||||||
|
await this.sendCommand(`MAIL FROM:<${this.email.from}>\r\n`, '250');
|
||||||
|
await this.sendCommand(`RCPT TO:<${this.email.to}>\r\n`, '250');
|
||||||
|
await this.sendCommand(`DATA\r\n`, '354');
|
||||||
|
|
||||||
|
// Now send the message content
|
||||||
|
await this.sendCommand(message);
|
||||||
|
await this.sendCommand('\r\n.\r\n', '250');
|
||||||
|
|
||||||
|
await this.sendCommand('QUIT\r\n', '221');
|
||||||
|
console.log('Email message sent successfully!');
|
||||||
|
}
|
||||||
|
}
|
69
ts/mta/mta.classes.emailsignjob.ts
Normal file
69
ts/mta/mta.classes.emailsignjob.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { MtaService } from './mta.classes.mta.js';
|
||||||
|
|
||||||
|
interface Headers {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEmailSignJobOptions {
|
||||||
|
domain: string;
|
||||||
|
selector: string;
|
||||||
|
headers: Headers;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailSignJob {
|
||||||
|
mtaRef: MtaService;
|
||||||
|
jobOptions: IEmailSignJobOptions;
|
||||||
|
|
||||||
|
constructor(mtaRefArg: MtaService, options: IEmailSignJobOptions) {
|
||||||
|
this.mtaRef = mtaRefArg;
|
||||||
|
this.jobOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPrivateKey(): Promise<string> {
|
||||||
|
return plugins.fs.promises.readFile(
|
||||||
|
(await this.mtaRef.dkimCreator.getKeyPathsForDomain(this.jobOptions.domain)).privateKeyPath,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSignatureHeader(emailMessage: string): Promise<string> {
|
||||||
|
const signResult = await plugins.dkimSign(emailMessage, {
|
||||||
|
// Optional, default canonicalization, default is "relaxed/relaxed"
|
||||||
|
canonicalization: 'relaxed/relaxed', // c=
|
||||||
|
|
||||||
|
// Optional, default signing and hashing algorithm
|
||||||
|
// Mostly useful when you want to use rsa-sha1, otherwise no need to set
|
||||||
|
algorithm: 'rsa-sha256',
|
||||||
|
|
||||||
|
// Optional, default is current time
|
||||||
|
signTime: new Date(), // t=
|
||||||
|
|
||||||
|
// Keys for one or more signatures
|
||||||
|
// Different signatures can use different algorithms (mostly useful when
|
||||||
|
// you want to sign a message both with RSA and Ed25519)
|
||||||
|
signatureData: [
|
||||||
|
{
|
||||||
|
signingDomain: this.jobOptions.domain, // d=
|
||||||
|
selector: this.jobOptions.selector, // s=
|
||||||
|
// supported key types: RSA, Ed25519
|
||||||
|
privateKey: await this.loadPrivateKey(), // k=
|
||||||
|
|
||||||
|
// Optional algorithm, default is derived from the key.
|
||||||
|
// Overrides whatever was set in parent object
|
||||||
|
algorithm: 'rsa-sha256',
|
||||||
|
|
||||||
|
// Optional signature specifc canonicalization, overrides whatever was set in parent object
|
||||||
|
canonicalization: 'relaxed/relaxed', // c=
|
||||||
|
|
||||||
|
// Maximum number of canonicalized body bytes to sign (eg. the "l=" tag).
|
||||||
|
// Do not use though. This is available only for compatibility testing.
|
||||||
|
// maxBodyLength: 12345
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const signature = signResult.signatures;
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
}
|
69
ts/mta/mta.classes.mta.ts
Normal file
69
ts/mta/mta.classes.mta.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
import { Email } from './mta.classes.email.js';
|
||||||
|
import { EmailSendJob } from './mta.classes.emailsendjob.js';
|
||||||
|
import { DKIMCreator } from './mta.classes.dkimcreator.js';
|
||||||
|
import { DKIMVerifier } from './mta.classes.dkimverifier.js';
|
||||||
|
import { SMTPServer } from './mta.classes.smtpserver.js';
|
||||||
|
import { DNSManager } from './mta.classes.dnsmanager.js';
|
||||||
|
import type { SzPlatformService } from '../classes.platformservice.js';
|
||||||
|
|
||||||
|
export class MtaService {
|
||||||
|
public platformServiceRef: SzPlatformService;
|
||||||
|
public server: SMTPServer;
|
||||||
|
public dkimCreator: DKIMCreator;
|
||||||
|
public dkimVerifier: DKIMVerifier;
|
||||||
|
public dnsManager: DNSManager;
|
||||||
|
|
||||||
|
constructor(platformServiceRefArg: SzPlatformService) {
|
||||||
|
this.platformServiceRef = platformServiceRefArg;
|
||||||
|
this.dkimCreator = new DKIMCreator(this);
|
||||||
|
this.dkimVerifier = new DKIMVerifier(this);
|
||||||
|
this.dnsManager = new DNSManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
// lets get the certificate
|
||||||
|
/**
|
||||||
|
* gets a certificate for a domain used by a service
|
||||||
|
* @param serviceNameArg
|
||||||
|
* @param domainNameArg
|
||||||
|
*/
|
||||||
|
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
const typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
||||||
|
typedrouter,
|
||||||
|
'https://cloudly.lossless.one:443'
|
||||||
|
);
|
||||||
|
const getCertificateForDomainOverHttps = async (domainNameArg: string) => {
|
||||||
|
const typedCertificateRequest =
|
||||||
|
typedsocketClient.createTypedRequest<any>('getSslCertificate');
|
||||||
|
const typedResponse = await typedCertificateRequest.fire({
|
||||||
|
authToken: '', // do proper auth here
|
||||||
|
requiredCertName: domainNameArg,
|
||||||
|
});
|
||||||
|
return typedResponse.certificate;
|
||||||
|
};
|
||||||
|
const certificate = await getCertificateForDomainOverHttps('mta.lossless.one');
|
||||||
|
await typedsocketClient.stop();
|
||||||
|
this.server = new SMTPServer(this, {
|
||||||
|
port: 25,
|
||||||
|
key: certificate.privateKey,
|
||||||
|
cert: certificate.publicKey,
|
||||||
|
});
|
||||||
|
await this.server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
if (!this.server) {
|
||||||
|
console.error('Server is not running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async send(email: Email): Promise<void> {
|
||||||
|
await this.dkimCreator.handleDKIMKeysForEmail(email);
|
||||||
|
const sendJob = new EmailSendJob(this, email);
|
||||||
|
await sendJob.send();
|
||||||
|
}
|
||||||
|
}
|
191
ts/mta/mta.classes.smtpserver.ts
Normal file
191
ts/mta/mta.classes.smtpserver.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import { Email } from './mta.classes.email.js';
|
||||||
|
import type { MtaService } from './mta.classes.mta.js';
|
||||||
|
|
||||||
|
export interface ISmtpServerOptions {
|
||||||
|
port: number;
|
||||||
|
key: string;
|
||||||
|
cert: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SMTPServer {
|
||||||
|
public mtaRef: MtaService;
|
||||||
|
private smtpServerOptions: ISmtpServerOptions;
|
||||||
|
private server: plugins.net.Server;
|
||||||
|
private emailBufferStringMap: Map<plugins.net.Socket, string>;
|
||||||
|
|
||||||
|
constructor(mtaRefArg: MtaService, optionsArg: ISmtpServerOptions) {
|
||||||
|
console.log('SMTPServer instance is being created...');
|
||||||
|
|
||||||
|
this.mtaRef = mtaRefArg;
|
||||||
|
this.smtpServerOptions = optionsArg;
|
||||||
|
this.emailBufferStringMap = new Map();
|
||||||
|
|
||||||
|
this.server = plugins.net.createServer((socket) => {
|
||||||
|
console.log('New connection established...');
|
||||||
|
|
||||||
|
socket.write('220 mta.lossless.one ESMTP Postfix\r\n');
|
||||||
|
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
this.processData(socket, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('end', () => {
|
||||||
|
console.log('Socket closed. Deleting related emailBuffer...');
|
||||||
|
socket.destroy();
|
||||||
|
this.emailBufferStringMap.delete(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', () => {
|
||||||
|
console.error('Socket error occurred. Deleting related emailBuffer...');
|
||||||
|
socket.destroy();
|
||||||
|
this.emailBufferStringMap.delete(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('close', () => {
|
||||||
|
console.log('Connection was closed by the client');
|
||||||
|
socket.destroy();
|
||||||
|
this.emailBufferStringMap.delete(socket);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTLS(socket: plugins.net.Socket) {
|
||||||
|
const secureContext = plugins.tls.createSecureContext({
|
||||||
|
key: this.smtpServerOptions.key,
|
||||||
|
cert: this.smtpServerOptions.cert,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tlsSocket = new plugins.tls.TLSSocket(socket, {
|
||||||
|
secureContext: secureContext,
|
||||||
|
isServer: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('secure', () => {
|
||||||
|
console.log('Connection secured.');
|
||||||
|
this.emailBufferStringMap.set(tlsSocket, this.emailBufferStringMap.get(socket) || '');
|
||||||
|
this.emailBufferStringMap.delete(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the same handler for the 'data' event as for the unsecured socket.
|
||||||
|
tlsSocket.on('data', (data: Buffer) => {
|
||||||
|
this.processData(tlsSocket, Buffer.from(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('end', () => {
|
||||||
|
console.log('TLS socket closed. Deleting related emailBuffer...');
|
||||||
|
this.emailBufferStringMap.delete(tlsSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('error', (err) => {
|
||||||
|
console.error('TLS socket error occurred. Deleting related emailBuffer...');
|
||||||
|
this.emailBufferStringMap.delete(tlsSocket);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: Buffer) {
|
||||||
|
const dataString = data.toString();
|
||||||
|
console.log(`Received data:`);
|
||||||
|
console.log(`${dataString}`)
|
||||||
|
|
||||||
|
if (dataString.startsWith('EHLO')) {
|
||||||
|
socket.write('250-mta.lossless.one Hello\r\n250 STARTTLS\r\n');
|
||||||
|
} else if (dataString.startsWith('MAIL FROM')) {
|
||||||
|
socket.write('250 Ok\r\n');
|
||||||
|
} else if (dataString.startsWith('RCPT TO')) {
|
||||||
|
socket.write('250 Ok\r\n');
|
||||||
|
} else if (dataString.startsWith('STARTTLS')) {
|
||||||
|
socket.write('220 Ready to start TLS\r\n');
|
||||||
|
this.startTLS(socket);
|
||||||
|
} else if (dataString.startsWith('DATA')) {
|
||||||
|
socket.write('354 End data with <CR><LF>.<CR><LF>\r\n');
|
||||||
|
let emailBuffer = this.emailBufferStringMap.get(socket);
|
||||||
|
if (!emailBuffer) {
|
||||||
|
this.emailBufferStringMap.set(socket, '');
|
||||||
|
}
|
||||||
|
} else if (dataString.startsWith('QUIT')) {
|
||||||
|
socket.write('221 Bye\r\n');
|
||||||
|
console.log('Received QUIT command, closing the socket...');
|
||||||
|
socket.destroy();
|
||||||
|
this.parseEmail(socket);
|
||||||
|
} else {
|
||||||
|
let emailBuffer = this.emailBufferStringMap.get(socket);
|
||||||
|
if (typeof emailBuffer === 'string') {
|
||||||
|
emailBuffer += dataString;
|
||||||
|
this.emailBufferStringMap.set(socket, emailBuffer);
|
||||||
|
}
|
||||||
|
socket.write('250 Ok\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataString.endsWith('\r\n.\r\n') ) { // End of data
|
||||||
|
console.log('Received end of data.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseEmail(socket: plugins.net.Socket | plugins.tls.TLSSocket) {
|
||||||
|
let emailData = this.emailBufferStringMap.get(socket);
|
||||||
|
// lets strip the end sequence
|
||||||
|
emailData = emailData?.replace(/\r\n\.\r\n$/, '');
|
||||||
|
|
||||||
|
plugins.smartfile.fs.ensureDirSync(paths.receivedEmailsDir);
|
||||||
|
plugins.smartfile.memory.toFsSync(emailData, plugins.path.join(paths.receivedEmailsDir, `${Date.now()}.eml`));
|
||||||
|
|
||||||
|
|
||||||
|
if (!emailData) {
|
||||||
|
console.error('No email data found for socket.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mightBeSpam = false;
|
||||||
|
|
||||||
|
// Verifying the email with DKIM
|
||||||
|
try {
|
||||||
|
const isVerified = await this.mtaRef.dkimVerifier.verify(emailData);
|
||||||
|
mightBeSpam = !isVerified;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to verify DKIM signature:', error);
|
||||||
|
mightBeSpam = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedEmail = await plugins.mailparser.simpleParser(emailData);
|
||||||
|
console.log(parsedEmail)
|
||||||
|
const email = new Email({
|
||||||
|
from: parsedEmail.from?.value[0].address || '',
|
||||||
|
to:
|
||||||
|
parsedEmail.to instanceof Array
|
||||||
|
? parsedEmail.to[0].value[0].address
|
||||||
|
: parsedEmail.to?.value[0].address,
|
||||||
|
subject: parsedEmail.subject || '',
|
||||||
|
text: parsedEmail.html || parsedEmail.text,
|
||||||
|
attachments:
|
||||||
|
parsedEmail.attachments?.map((attachment) => ({
|
||||||
|
filename: attachment.filename || '',
|
||||||
|
content: attachment.content,
|
||||||
|
contentType: attachment.contentType,
|
||||||
|
})) || [],
|
||||||
|
mightBeSpam: mightBeSpam,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('mail received!');
|
||||||
|
console.log(email);
|
||||||
|
|
||||||
|
this.emailBufferStringMap.delete(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this.server.listen(this.smtpServerOptions.port, () => {
|
||||||
|
console.log(`SMTP Server is now running on port ${this.smtpServerOptions.port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.server.getConnections((err, count) => {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log('Number of active connections: ', count);
|
||||||
|
});
|
||||||
|
this.server.close(() => {
|
||||||
|
console.log('SMTP Server is now stopped');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
12
ts/paths.ts
Normal file
12
ts/paths.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
export const packageDir = plugins.path.join(
|
||||||
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
|
'../'
|
||||||
|
);
|
||||||
|
export const assetsDir = plugins.path.join(packageDir, './assets');
|
||||||
|
export const keysDir = plugins.path.join(assetsDir, './keys');
|
||||||
|
export const dnsRecordsDir = plugins.path.join(assetsDir, './dns-records');
|
||||||
|
export const sentEmailsDir = plugins.path.join(assetsDir, './sent-emails');
|
||||||
|
export const receivedEmailsDir = plugins.path.join(assetsDir, './received-emails');
|
||||||
|
plugins.smartfile.fs.ensureDirSync(keysDir);
|
@ -1,6 +0,0 @@
|
|||||||
import * as plugins from './platformservice.plugins.js';
|
|
||||||
|
|
||||||
export const packageDir = plugins.path.join(
|
|
||||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
|
||||||
'../'
|
|
||||||
);
|
|
@ -1,24 +0,0 @@
|
|||||||
// node native
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
export {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
// @api.global scope
|
|
||||||
import * as typedrequest from '@api.global/typedrequest';
|
|
||||||
import * as typedserver from '@api.global/typedserver';
|
|
||||||
|
|
||||||
export {
|
|
||||||
typedrequest,
|
|
||||||
typedserver,
|
|
||||||
}
|
|
||||||
|
|
||||||
// pushrocks scope
|
|
||||||
// pushrocks scope
|
|
||||||
import * as projectinfo from '@push.rocks/projectinfo';
|
|
||||||
import * as qenv from '@push.rocks/qenv';
|
|
||||||
import * as smartdata from '@push.rocks/smartdata';
|
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
|
||||||
|
|
||||||
export { projectinfo, qenv, smartdata, smartpath };
|
|
77
ts/plugins.ts
Normal file
77
ts/plugins.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// node native
|
||||||
|
import * as dns from 'dns';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as net from 'net';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as tls from 'tls';
|
||||||
|
import * as util from 'util';
|
||||||
|
|
||||||
|
export {
|
||||||
|
dns,
|
||||||
|
fs,
|
||||||
|
crypto,
|
||||||
|
net,
|
||||||
|
path,
|
||||||
|
tls,
|
||||||
|
util,
|
||||||
|
}
|
||||||
|
|
||||||
|
// @serve.zone scope
|
||||||
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
export {
|
||||||
|
servezoneInterfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// @api.global scope
|
||||||
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
|
import * as typedserver from '@api.global/typedserver';
|
||||||
|
import * as typedsocket from '@api.global/typedsocket';
|
||||||
|
|
||||||
|
export {
|
||||||
|
typedrequest,
|
||||||
|
typedserver,
|
||||||
|
typedsocket,
|
||||||
|
}
|
||||||
|
|
||||||
|
// @push.rocks scope
|
||||||
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
|
import * as qenv from '@push.rocks/qenv';
|
||||||
|
import * as smartdata from '@push.rocks/smartdata';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
|
import * as smartmail from '@push.rocks/smartmail';
|
||||||
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
|
|
||||||
|
export { projectinfo, qenv, smartdata, smartfile, smartlog, smartmail, smartpath, smartpromise, smartrequest, smartrx };
|
||||||
|
|
||||||
|
// apiclient.xyz scope
|
||||||
|
import * as letterxpress from '@apiclient.xyz/letterxpress';
|
||||||
|
|
||||||
|
export {
|
||||||
|
letterxpress,
|
||||||
|
}
|
||||||
|
|
||||||
|
// tsclass scope
|
||||||
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
|
|
||||||
|
export {
|
||||||
|
tsclass,
|
||||||
|
}
|
||||||
|
|
||||||
|
// third party
|
||||||
|
import * as mailauth from 'mailauth';
|
||||||
|
import { dkimSign } from 'mailauth/lib/dkim/sign.js';
|
||||||
|
import mailparser from 'mailparser';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
|
||||||
|
export {
|
||||||
|
mailauth,
|
||||||
|
dkimSign,
|
||||||
|
mailparser,
|
||||||
|
uuid,
|
||||||
|
}
|
1
ts/sms/index.ts
Normal file
1
ts/sms/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './smsservice.js';
|
89
ts/sms/smsservice.ts
Normal file
89
ts/sms/smsservice.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { SzPlatformService } from '../classes.platformservice.js';
|
||||||
|
|
||||||
|
export interface ISmsConstructorOptions {
|
||||||
|
apiGatewayApiToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SmsService {
|
||||||
|
public platformServiceRef: SzPlatformService;
|
||||||
|
public projectinfo: plugins.projectinfo.ProjectInfo;
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public options: ISmsConstructorOptions;
|
||||||
|
|
||||||
|
constructor(platformServiceRefArg: SzPlatformService, optionsArg: ISmsConstructorOptions) {
|
||||||
|
this.platformServiceRef = platformServiceRefArg;
|
||||||
|
this.options = optionsArg;
|
||||||
|
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts the financeflow instance
|
||||||
|
*/
|
||||||
|
public async start() {
|
||||||
|
logger.log('info', `starting sms service`);
|
||||||
|
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.platformservice.sms.IRequest_SendSms>(
|
||||||
|
'sendSms',
|
||||||
|
async (reqData) => {
|
||||||
|
await this.sendSms(reqData.toNumber, reqData.fromName, reqData.messageText);
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.platformservice.sms.IRequest_SendVerificationCode>(
|
||||||
|
'sendVerificationCode',
|
||||||
|
async (reqData) => {
|
||||||
|
const verificationCode = (
|
||||||
|
await this.sendVerificationCode(reqData.toNumber, reqData.fromName)
|
||||||
|
).toString();
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
verificationCode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendSms(toNumber: number, fromName: string, messageText: string) {
|
||||||
|
const payload = {
|
||||||
|
sender: fromName,
|
||||||
|
message: messageText,
|
||||||
|
recipients: [{ msisdn: toNumber }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await plugins.smartrequest.request('https://gatewayapi.com/rest/mtsms', {
|
||||||
|
method: 'POST',
|
||||||
|
requestBody: JSON.stringify(payload),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`${this.options.apiGatewayApiToken}:`).toString('base64')}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const json = await resp.body;
|
||||||
|
logger.log('info', `sent an sms to ${toNumber} with text '${messageText}'`, {
|
||||||
|
eventType: 'sentSms',
|
||||||
|
sms: {
|
||||||
|
fromName: fromName,
|
||||||
|
toNumber: toNumber.toString(),
|
||||||
|
messageText: messageText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(json, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendVerificationCode(toNumber: number, fromName: string) {
|
||||||
|
let verificationCode = Math.floor(100000 + Math.random() * 900000);
|
||||||
|
await this.sendSms(toNumber, fromName, `Your verification code: ${verificationCode}`);
|
||||||
|
return verificationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {}
|
||||||
|
}
|
Reference in New Issue
Block a user