fix(core): update

This commit is contained in:
Philipp Kunz 2024-02-16 13:28:40 +01:00
parent 6bd396e2d6
commit f5a36ab53a
32 changed files with 1051 additions and 119 deletions

View File

@ -26,7 +26,12 @@
"@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.0.5",
"@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/smartstate": "^2.0.0"
"@push.rocks/smartrequest": "^2.0.21",
"@push.rocks/smartstate": "^2.0.0",
"@serve.zone/interfaces": "^1.0.34"
}
}

View File

@ -20,12 +20,27 @@ dependencies:
'@push.rocks/smartdata':
specifier: ^5.0.7
version: 5.0.33
'@push.rocks/smartfile':
specifier: ^11.0.4
version: 11.0.4
'@push.rocks/smartlog':
specifier: ^3.0.3
version: 3.0.3
'@push.rocks/smartmail':
specifier: ^1.0.24
version: 1.0.24
'@push.rocks/smartpath':
specifier: ^5.0.5
version: 5.0.11
'@push.rocks/smartrequest':
specifier: ^2.0.21
version: 2.0.21
'@push.rocks/smartstate':
specifier: ^2.0.0
version: 2.0.17
'@serve.zone/interfaces':
specifier: ^1.0.34
version: 1.0.34
devDependencies:
'@git.zone/tsbuild':
@ -1321,6 +1336,17 @@ packages:
dependencies:
'@push.rocks/smartpromise': 4.0.3
/@push.rocks/smartdns@5.0.4:
resolution: {integrity: sha512-DCeekCzAJEPn8gZOPZLnFGFZdgLKtCqb3YfwJpfkdSfPxwlSXzjs5rYmzmsg0BlQP+7pSR43fuLMERB4DGCn9w==}
dependencies:
'@pushrocks/smartdelay': 2.0.13
'@pushrocks/smartenv': 5.0.5
'@pushrocks/smartpromise': 3.1.10
'@pushrocks/smartrequest': 2.0.15
'@tsclass/tsclass': 4.0.51
dns2: 2.1.0
dev: false
/@push.rocks/smartenv@5.0.12:
resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==}
dependencies:
@ -1425,6 +1451,16 @@ packages:
'@push.rocks/isounique': 1.0.5
'@push.rocks/smartlog-interfaces': 3.0.0
/@push.rocks/smartmail@1.0.24:
resolution: {integrity: sha512-UUIKQmMiCpcnkql4uoPp0kDRDnGl1jV5UbacRkoaKHh6PPd4trkZRVcVQadtpEqteFb6W0B9Z/YtMKpTPxUsxA==}
dependencies:
'@push.rocks/smartdns': 5.0.4
'@push.rocks/smartfile': 10.0.41
'@push.rocks/smartmustache': 3.0.2
'@push.rocks/smartpath': 5.0.11
'@push.rocks/smartrequest': 2.0.21
dev: false
/@push.rocks/smartmanifest@2.0.2:
resolution: {integrity: sha512-QGc5C9vunjfUbYsPGz5bynV/mVmPHkrQDkWp8ZO8VJtK1GZe+njgbrNyxn2SUHR0IhSAbSXl1j4JvBqYf5eTVg==}
@ -1457,6 +1493,12 @@ packages:
- supports-color
dev: false
/@push.rocks/smartmustache@3.0.2:
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
dependencies:
handlebars: 4.7.8
dev: false
/@push.rocks/smartnetwork@3.0.2:
resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==}
dependencies:
@ -1933,6 +1975,16 @@ packages:
form-data: 4.0.0
dev: false
/@pushrocks/smartrequest@2.0.15:
resolution: {integrity: sha512-QDXXKhOwAIp+TNFrDglApBpbCTClHrf8pUM3w81q5+VtrMbqUrLINHhInXt3ZUSgTS7RD9HtJSIVSqAukcJo5A==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartrequest
dependencies:
'@pushrocks/smartpromise': 4.0.2
'@pushrocks/smarturl': 3.0.6
agentkeepalive: 4.5.0
form-data: 4.0.0
dev: false
/@pushrocks/smartrx@2.0.27:
resolution: {integrity: sha512-aFRpGxDZgHH1mpmkRBTFwuIVqFiDxk22n2vX2gW4hntV0nJGlt9M9dixMFFXGUjabwX9hHW7y60QPJm2rKaypA==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartrx
@ -2003,12 +2055,25 @@ packages:
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarturl
dev: false
/@pushrocks/smarturl@3.0.6:
resolution: {integrity: sha512-YHWnYE1mm8WIJk1usSXg+kNM7s7ByM+PKApO9cgl0T/oGybjzAJiO3clGNDro4ysP0TD+WuvJuiVue02bErEBQ==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarturl
dev: false
/@rkusa/linebreak@1.0.0:
resolution: {integrity: sha512-yCSm87XA1aYMgfcABSxcIkk3JtCw3AihNceHY+DnZGLvVP/g2z3UWZbi0xIoYpZWAJEVPr5Zt3QE37Q80wF1pA==}
dependencies:
unicode-trie: 0.3.1
dev: true
/@serve.zone/interfaces@1.0.34:
resolution: {integrity: sha512-5G340VReyWT8v4DNMUOfWQtZSDAA+sCnWwhMUaVcGglMSSMpPge6N3G7M7gbRknf87nqN13U/6+ZzVqRz3U52w==}
dependencies:
'@apiglobal/typedrequest-interfaces': 2.0.1
'@push.rocks/smartlog-interfaces': 3.0.0
'@tsclass/tsclass': 4.0.51
dev: false
/@sindresorhus/is@5.6.0:
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
engines: {node: '>=14.16'}
@ -3731,6 +3796,10 @@ packages:
dns-packet: 5.6.1
dev: true
/dns2@2.1.0:
resolution: {integrity: sha512-m27K11aQalRbmUs7RLaz6aPyceLjAoqjPRNTdE7qUouQpl+PC8Bi67O+i9SuJUPbQC8dxFrczAxfmTPuTKHNkw==}
dev: false
/domexception@1.0.1:
resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==}
dependencies:
@ -4360,6 +4429,19 @@ packages:
engines: {node: '>=4.x'}
dev: true
/handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
hasBin: true
dependencies:
minimist: 1.2.8
neo-async: 2.6.2
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
uglify-js: 3.17.4
dev: false
/has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
@ -5190,7 +5272,6 @@ packages:
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
/minipass@7.0.4:
resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
@ -5361,6 +5442,10 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
/neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: false
/new-find-package-json@2.0.0:
resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==}
engines: {node: '>=12.22.0'}
@ -6131,7 +6216,6 @@ packages:
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: true
/source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
@ -6540,7 +6624,6 @@ packages:
resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
engines: {node: '>=0.8.0'}
hasBin: true
dev: true
/unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@ -6706,6 +6789,10 @@ packages:
string-width: 2.1.1
dev: true
/wordwrap@1.0.0:
resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=}
dev: false
/wrap-ansi@5.1.0:
resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
engines: {node: '>=6'}

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/platformservice',
version: '1.0.2',
version: '1.0.3',
description: 'contains the platformservice container with mail, sms, letter, ai services.'
}

View File

@ -1,5 +1,5 @@
import * as plugins from './platformservice.plugins.js';
import * as paths from './platformservice.paths.js';
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { PlatformServiceDb } from './classes.platformservicedb.js'
export class SzPlatformService {

View File

@ -1,4 +1,4 @@
import * as plugins from './platformservice.plugins.js';
import * as plugins from './plugins.js';
import { SzPlatformService } from './classes.platformservice.js';

View File

@ -1,17 +1,16 @@
import * as plugins from './email.plugins.js';
import { Email } from './email.classes.email.js';
import { request } from 'http';
import { logger } from './email.logging.js';
import * as plugins from '../plugins.js';
import { EmailService } from './email.classes.emailservice.js';
import { logger } from '../logger.js';
export class ApiManager {
public emailRef: Email;
public emailRef: EmailService;
public typedRouter = new plugins.typedrequest.TypedRouter();
constructor(emailRefArg: Email) {
constructor(emailRefArg: EmailService) {
this.emailRef = emailRefArg;
this.emailRef.mainTypedRouter.addTypedRouter(this.typedRouter);
this.typedRouter.addTypedHandler<plugins.lointEmail.IRequestSendEmail>(
this.emailRef.typedrouter.addTypedRouter(this.typedRouter);
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.platformservice.mta.IRequest_SendEmail>(
new plugins.typedrequest.TypedHandler('sendEmail', async (requestData) => {
const mailToSend = new plugins.smartmail.Smartmail({
body: requestData.body,
@ -22,7 +21,7 @@ export class ApiManager {
if (requestData.attachments) {
for (const attachment of requestData.attachments) {
mailToSend.addAttachment(
await plugins.smartfile.Smartfile.fromString(
await plugins.smartfile.SmartFile.fromString(
attachment.name,
attachment.binaryAttachmentString,
'binary'

View File

@ -1,11 +1,11 @@
import * as plugins from './email.plugins.js';
import { Email } from './email.classes.email.js';
import { EmailService } from './email.classes.emailservice.js';
export class MailgunConnector {
public emailRef: Email;
public emailRef: EmailService;
public mailgunAccount: plugins.mailgun.MailgunAccount;
constructor(emailRefArg: Email) {
constructor(emailRefArg: EmailService) {
this.emailRef = emailRefArg;
this.mailgunAccount = new plugins.mailgun.MailgunAccount({
apiToken: this.emailRef.qenv.getEnvVarOnDemand('MAILGUN_API_TOKEN'),

View File

@ -1,16 +1,16 @@
import * as plugins from './email.plugins.js';
import * as paths from './email.paths.js';
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { MailgunConnector } from './email.classes.connector.mailgun.js';
import { RuleManager } from './email.classes.rulemanager.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';
export class Email {
export class EmailService {
public platformServiceRef: SzPlatformService;
// typedrouter
public mainTypedRouter = new plugins.typedrequest.TypedRouter();
public typedrouter = new plugins.typedrequest.TypedRouter();
// connectors
public mailgunConnector: MailgunConnector;
@ -22,11 +22,12 @@ export class Email {
constructor(platformServiceRefArg: SzPlatformService) {
this.platformServiceRef = platformServiceRefArg;
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
this.mailgunConnector = new MailgunConnector(this);
this.ruleManager = new RuleManager(this);
this.platformServiceRef.typedserver.server.addRoute(
'/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');
res.status(200);
res.end();

View File

@ -1,14 +1,14 @@
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';
export class RuleManager {
public emailRef: Email;
public emailRef: EmailService;
public smartruleInstance = new plugins.smartrule.SmartRule<
plugins.smartmail.Smartmail<plugins.mailgun.IMailgunMessage>
>();
constructor(emailRefArg: Email) {
constructor(emailRefArg: EmailService) {
this.emailRef = emailRefArg;
}

View File

@ -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',
});

View File

@ -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,
};

View File

@ -1,3 +1,3 @@
import { Email } from './email.classes.email.js';
import { EmailService } from './email.classes.emailservice.js';
export { Email };
export { EmailService as Email };

View File

@ -1,4 +1,4 @@
export * from './00_commitinfo_data.js';
import { SzPlatformService } from './classes.platformservice.js'
import { SzPlatformService } from './classes.platformservice.js';
export const runCli = async () => {}

9
ts/logger.ts Normal file
View 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
View 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';

View File

@ -0,0 +1,7 @@
import * as plugins from './plugins.js';
export class ApiManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
}

View 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 { MTA } 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: MTA, 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,
};
}
}

View File

@ -0,0 +1,35 @@
import * as plugins from './plugins.js';
import { MTA } from './mta.classes.mta.js';
class DKIMVerifier {
public mtaRef: MTA;
constructor(mtaRefArg: MTA) {
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 };

View File

@ -0,0 +1,10 @@
import type { MTA } from './mta.classes.mta.js';
import * as plugins from './plugins.js';
export class DNSManager {
public mtaRef: MTA;
constructor(mtaRefArg: MTA) {
this.mtaRef = mtaRefArg;
}
}

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

View 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 { MTA } from './mta.classes.mta.js';
export class EmailSendJob {
mtaRef: MTA;
private email: Email;
private socket: plugins.net.Socket | plugins.tls.TLSSocket = null;
private mxRecord: string = null;
constructor(mtaRef: MTA, 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!');
}
}

View File

@ -0,0 +1,69 @@
import * as plugins from './plugins.js';
import type { MTA } from './mta.classes.mta.js';
interface Headers {
[key: string]: string;
}
interface IEmailSignJobOptions {
domain: string;
selector: string;
headers: Headers;
body: string;
}
export class EmailSignJob {
mtaRef: MTA;
jobOptions: IEmailSignJobOptions;
constructor(mtaRefArg: MTA, 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;
}
}

66
ts/mta/mta.classes.mta.ts Normal file
View File

@ -0,0 +1,66 @@
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';
export class MTA {
public server: SMTPServer;
public dkimCreator: DKIMCreator;
public dkimVerifier: DKIMVerifier;
public dnsManager: DNSManager;
constructor() {
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();
}
}

View 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 { MTA } from './mta.classes.mta.js';
export interface ISmtpServerOptions {
port: number;
key: string;
cert: string;
}
export class SMTPServer {
public mtaRef: MTA;
private smtpServerOptions: ISmtpServerOptions;
private server: plugins.net.Server;
private emailBufferStringMap: Map<plugins.net.Socket, string>;
constructor(mtaRefArg: MTA, 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/mta/paths.ts Normal file
View File

@ -0,0 +1,12 @@
import * as plugins from './plugins.js';
export const cwd = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../'
);
export const assetsDir = plugins.path.join(cwd, './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);

67
ts/mta/plugins.ts Normal file
View File

@ -0,0 +1,67 @@
// 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,
}
// @apiclient.xyz/cloudflare
import * as cloudflare from '@apiclient.xyz/cloudflare';
export {
cloudflare,
}
// @apiglobal scope
import * as typedrequest from '@apiglobal/typedrequest';
import * as typedsocket from '@apiglobal/typedsocket';
export {
typedrequest,
typedsocket,
}
// pushrocks scope
import * as smartfile from '@pushrocks/smartfile';
import * as smartpath from '@pushrocks/smartpath';
import * as smartpromise from '@pushrocks/smartpromise';
import * as smartrx from '@pushrocks/smartrx';
export {
smartfile,
smartpath,
smartpromise,
smartrx,
}
// 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,
}

View File

@ -1,4 +1,4 @@
import * as plugins from './email.plugins.js';
import * as plugins from './plugins.js';
export const packageDir = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),

View File

@ -1,6 +0,0 @@
import * as plugins from './platformservice.plugins.js';
export const packageDir = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../'
);

View File

@ -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 };

34
ts/plugins.ts Normal file
View File

@ -0,0 +1,34 @@
// node native
import * as path from 'path';
export {
path
}
// @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';
export {
typedrequest,
typedserver,
}
// @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 smartrequest from '@push.rocks/smartrequest';
export { projectinfo, qenv, smartdata, smartfile, smartlog, smartmail, smartpath, smartrequest };

1
ts/sms/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './smsservice.js';

89
ts/sms/smsservice.ts Normal file
View 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 {
apiToken: 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.apiToken}:`).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() {}
}