@serve.zone/platformclient 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +17 -6
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.platformclient.d.ts +1 -1
- package/dist_ts/classes.platformclient.js +1 -1
- package/dist_ts/email/classes.emailconnector.d.ts +85 -1
- package/dist_ts/email/classes.emailconnector.js +228 -1
- package/dist_ts/email/classes.letterconnector.d.ts +1 -1
- package/dist_ts/email/classes.letterconnector.js +2 -1
- package/dist_ts/email/classes.pushnotificationconnector.js +2 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/plugins.d.ts +1 -0
- package/dist_ts/plugins.js +2 -1
- package/dist_ts_infohtml/plugins.js +1 -2
- package/dist_ts_infohtml/template.js +11 -8
- package/package.json +14 -13
- package/readme.md +106 -73
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.platformclient.ts +1 -1
- package/ts/email/classes.emailconnector.ts +322 -0
- package/ts/email/classes.letterconnector.ts +1 -0
- package/ts/email/classes.pushnotificationconnector.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/plugins.ts +2 -0
|
@@ -3,6 +3,80 @@ import * as plugins from '../plugins.js';
|
|
|
3
3
|
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
5
|
|
|
6
|
+
export type TServiceMailTlsMode = 'plain' | 'starttls' | 'implicitTls';
|
|
7
|
+
|
|
8
|
+
export interface IServiceMailSmtpCredentials {
|
|
9
|
+
from: string;
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
tlsMode: TServiceMailTlsMode;
|
|
13
|
+
username: string;
|
|
14
|
+
password: string;
|
|
15
|
+
addressToken?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IServiceMailStatus {
|
|
19
|
+
ready: boolean;
|
|
20
|
+
from?: string;
|
|
21
|
+
host?: string;
|
|
22
|
+
port?: number;
|
|
23
|
+
tlsMode?: TServiceMailTlsMode;
|
|
24
|
+
usernameConfigured: boolean;
|
|
25
|
+
passwordConfigured: boolean;
|
|
26
|
+
missing: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IServiceMailAttachment {
|
|
30
|
+
filename: string;
|
|
31
|
+
contentType?: string;
|
|
32
|
+
content: string | Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IServiceMailSendOptions {
|
|
36
|
+
from?: string;
|
|
37
|
+
to: string | string[];
|
|
38
|
+
cc?: string | string[];
|
|
39
|
+
bcc?: string | string[];
|
|
40
|
+
subject: string;
|
|
41
|
+
text?: string;
|
|
42
|
+
html?: string;
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
attachments?: IServiceMailAttachment[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IServiceMailSendResult {
|
|
48
|
+
messageId?: string;
|
|
49
|
+
accepted?: string[];
|
|
50
|
+
rejected?: string[];
|
|
51
|
+
response?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IServiceMailInboundMessage extends plugins.servezoneInterfaces.data.IMailInboundMessagePayload {
|
|
55
|
+
from: string;
|
|
56
|
+
to: string[];
|
|
57
|
+
receivedAtIso: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IServiceMailInboundHandlerResult {
|
|
61
|
+
accepted?: boolean;
|
|
62
|
+
workAppMessageId?: string;
|
|
63
|
+
message?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type IServiceMailInboundHandleResult =
|
|
67
|
+
plugins.servezoneInterfaces.requests.mail.IReq_DeliverInboundMail['response'];
|
|
68
|
+
|
|
69
|
+
interface INodemailerSendResult {
|
|
70
|
+
messageId?: string;
|
|
71
|
+
accepted?: unknown[];
|
|
72
|
+
rejected?: unknown[];
|
|
73
|
+
response?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type TServiceMailInboundHandler = (
|
|
77
|
+
messageArg: IServiceMailInboundMessage,
|
|
78
|
+
) => Promise<IServiceMailInboundHandlerResult | void> | IServiceMailInboundHandlerResult | void;
|
|
79
|
+
|
|
6
80
|
export class SzEmailConnector {
|
|
7
81
|
public platformClientRef: SzPlatformClient;
|
|
8
82
|
|
|
@@ -10,6 +84,9 @@ export class SzEmailConnector {
|
|
|
10
84
|
this.platformClientRef = platformClientRefArg;
|
|
11
85
|
}
|
|
12
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Legacy platform-service email path. New service-mail code should use sendMail().
|
|
89
|
+
*/
|
|
13
90
|
public async sendEmail(
|
|
14
91
|
optionsArg: plugins.servezoneInterfaces.platform.email.IReq_SendEmail['request']
|
|
15
92
|
) {
|
|
@@ -30,10 +107,255 @@ export class SzEmailConnector {
|
|
|
30
107
|
'sendEmail'
|
|
31
108
|
);
|
|
32
109
|
const response = await typedRequest.fire(optionsArg);
|
|
110
|
+
return response.responseId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async getServiceMailStatus(addressArg?: string): Promise<IServiceMailStatus> {
|
|
114
|
+
const requestedAddress = addressArg?.trim().toLowerCase();
|
|
115
|
+
const token = requestedAddress ? this.getMailEnvToken(requestedAddress) : undefined;
|
|
116
|
+
const scopedEnv = token ? await this.readMailEnv(token) : undefined;
|
|
117
|
+
const defaultEnv = token ? await this.readMailEnv() : undefined;
|
|
118
|
+
const useDefaultEnv = requestedAddress
|
|
119
|
+
&& !this.hasAnyMailEnvValue(scopedEnv)
|
|
120
|
+
&& defaultEnv?.from?.trim().toLowerCase() === requestedAddress;
|
|
121
|
+
const env = useDefaultEnv ? defaultEnv! : scopedEnv || await this.readMailEnv();
|
|
122
|
+
const missingPrefix = useDefaultEnv || !token ? undefined : token;
|
|
123
|
+
const port = env.smtpPort ? Number(env.smtpPort) : undefined;
|
|
124
|
+
const tlsMode = this.normalizeTlsMode(env.smtpTlsMode || 'starttls');
|
|
125
|
+
const missing: string[] = [];
|
|
126
|
+
if (!env.from) missing.push(missingPrefix ? `MAIL_${missingPrefix}_FROM` : 'MAIL_FROM');
|
|
127
|
+
if (!env.smtpHost) missing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_HOST` : 'SMTP_HOST');
|
|
128
|
+
if (!env.smtpPort || !Number.isInteger(port) || port! < 1 || port! > 65535) {
|
|
129
|
+
missing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_PORT` : 'SMTP_PORT');
|
|
130
|
+
}
|
|
131
|
+
if (!env.smtpUsername) missing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_USERNAME` : 'SMTP_USERNAME');
|
|
132
|
+
if (!env.smtpPassword) missing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_PASSWORD` : 'SMTP_PASSWORD');
|
|
133
|
+
return {
|
|
134
|
+
ready: missing.length === 0,
|
|
135
|
+
from: env.from,
|
|
136
|
+
host: env.smtpHost,
|
|
137
|
+
port,
|
|
138
|
+
tlsMode,
|
|
139
|
+
usernameConfigured: Boolean(env.smtpUsername),
|
|
140
|
+
passwordConfigured: Boolean(env.smtpPassword),
|
|
141
|
+
missing,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public async getServiceMailCredentials(addressArg?: string): Promise<IServiceMailSmtpCredentials> {
|
|
146
|
+
const requestedAddress = addressArg?.trim().toLowerCase();
|
|
147
|
+
const requestedToken = requestedAddress ? this.getMailEnvToken(requestedAddress) : undefined;
|
|
148
|
+
const scopedEnv = requestedToken ? await this.readMailEnv(requestedToken) : undefined;
|
|
149
|
+
const hasScopedConfig = scopedEnv && Object.values(scopedEnv).some(Boolean);
|
|
150
|
+
const env = hasScopedConfig ? scopedEnv! : await this.readMailEnv();
|
|
151
|
+
const from = env.from?.trim().toLowerCase();
|
|
152
|
+
if (!from) throw new Error('MAIL_FROM is required for service mail');
|
|
153
|
+
if (requestedAddress && from !== requestedAddress) {
|
|
154
|
+
throw new Error(`No service mail credentials are configured for ${requestedAddress}`);
|
|
155
|
+
}
|
|
156
|
+
const port = Number(env.smtpPort);
|
|
157
|
+
if (!env.smtpHost) throw new Error('SMTP_HOST is required for service mail');
|
|
158
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
159
|
+
throw new Error('SMTP_PORT must be an integer between 1 and 65535');
|
|
160
|
+
}
|
|
161
|
+
if (!env.smtpUsername) throw new Error('SMTP_USERNAME is required for service mail');
|
|
162
|
+
if (!env.smtpPassword) throw new Error('SMTP_PASSWORD is required for service mail');
|
|
163
|
+
return {
|
|
164
|
+
from,
|
|
165
|
+
host: env.smtpHost,
|
|
166
|
+
port,
|
|
167
|
+
tlsMode: this.normalizeTlsMode(env.smtpTlsMode || 'starttls'),
|
|
168
|
+
username: env.smtpUsername,
|
|
169
|
+
password: env.smtpPassword,
|
|
170
|
+
addressToken: hasScopedConfig ? requestedToken : undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async sendText(optionsArg: Omit<IServiceMailSendOptions, 'html'> & { text: string }) {
|
|
175
|
+
return await this.sendMail(optionsArg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public async sendHtml(optionsArg: Omit<IServiceMailSendOptions, 'text'> & { html: string }) {
|
|
179
|
+
return await this.sendMail(optionsArg);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public async sendMail(optionsArg: IServiceMailSendOptions): Promise<IServiceMailSendResult> {
|
|
183
|
+
if (!optionsArg.text && !optionsArg.html) {
|
|
184
|
+
throw new Error('Either text or html is required for service mail');
|
|
185
|
+
}
|
|
186
|
+
if (this.platformClientRef.debugMode) {
|
|
187
|
+
logger.log('info', `service mail debug send: ${optionsArg.subject} to ${this.listToArray(optionsArg.to).join(', ')}`);
|
|
188
|
+
return { messageId: 'debug' };
|
|
189
|
+
}
|
|
33
190
|
|
|
191
|
+
const credentials = await this.getServiceMailCredentials(optionsArg.from);
|
|
192
|
+
const smartsmtp = await plugins.getSmartsmtp();
|
|
193
|
+
const smtp = await smartsmtp.Smartsmtp.createSmartsmtpWithRelay({
|
|
194
|
+
smtpServer: credentials.host,
|
|
195
|
+
smtpPort: credentials.port,
|
|
196
|
+
smtpTlsMode: credentials.tlsMode,
|
|
197
|
+
smtpUser: credentials.username,
|
|
198
|
+
smtpPassword: credentials.password,
|
|
199
|
+
});
|
|
200
|
+
const result = (await smtp.nodemailerTransport.sendMail({
|
|
201
|
+
from: credentials.from,
|
|
202
|
+
to: this.listToArray(optionsArg.to),
|
|
203
|
+
cc: optionsArg.cc ? this.listToArray(optionsArg.cc) : undefined,
|
|
204
|
+
bcc: optionsArg.bcc ? this.listToArray(optionsArg.bcc) : undefined,
|
|
205
|
+
subject: optionsArg.subject,
|
|
206
|
+
text: optionsArg.text,
|
|
207
|
+
html: optionsArg.html,
|
|
208
|
+
headers: optionsArg.headers,
|
|
209
|
+
attachments: optionsArg.attachments?.map((attachmentArg) => ({
|
|
210
|
+
filename: attachmentArg.filename,
|
|
211
|
+
contentType: attachmentArg.contentType,
|
|
212
|
+
content: this.normalizeAttachmentContent(attachmentArg.content),
|
|
213
|
+
})),
|
|
214
|
+
})) as INodemailerSendResult;
|
|
215
|
+
return {
|
|
216
|
+
messageId: result.messageId,
|
|
217
|
+
accepted: Array.isArray(result.accepted) ? result.accepted.map(String) : undefined,
|
|
218
|
+
rejected: Array.isArray(result.rejected) ? result.rejected.map(String) : undefined,
|
|
219
|
+
response: result.response,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public normalizeInboundMessage(payloadArg: unknown): IServiceMailInboundMessage {
|
|
224
|
+
const payload = this.asRecord(payloadArg);
|
|
225
|
+
const deliveryCandidate = this.asRecord(payload.delivery || payload.message || payload);
|
|
226
|
+
const envelope = this.asRecord(deliveryCandidate.envelope);
|
|
227
|
+
const rawMessage = typeof deliveryCandidate.rawMessage === 'string' ? deliveryCandidate.rawMessage : '';
|
|
228
|
+
const rcptTo = Array.isArray(envelope.rcptTo) ? envelope.rcptTo.map(String) : [];
|
|
229
|
+
const receivedAt = typeof deliveryCandidate.receivedAt === 'number' ? deliveryCandidate.receivedAt : Date.now();
|
|
230
|
+
return {
|
|
231
|
+
spoolItemId: String(deliveryCandidate.spoolItemId || deliveryCandidate.id || ''),
|
|
232
|
+
owner: deliveryCandidate.owner as plugins.servezoneInterfaces.data.IMailResourceOwner | undefined,
|
|
233
|
+
envelope: {
|
|
234
|
+
mailFrom: String(envelope.mailFrom || deliveryCandidate.from || ''),
|
|
235
|
+
rcptTo,
|
|
236
|
+
},
|
|
237
|
+
rawMessage,
|
|
238
|
+
sizeBytes: typeof deliveryCandidate.sizeBytes === 'number' ? deliveryCandidate.sizeBytes : rawMessage.length,
|
|
239
|
+
messageId: deliveryCandidate.messageId ? String(deliveryCandidate.messageId) : undefined,
|
|
240
|
+
subject: deliveryCandidate.subject ? String(deliveryCandidate.subject) : this.extractHeader(rawMessage, 'subject'),
|
|
241
|
+
headers: this.normalizeHeaders(deliveryCandidate.headers),
|
|
242
|
+
source: deliveryCandidate.source as plugins.servezoneInterfaces.data.IMailConnectionInfo | undefined,
|
|
243
|
+
receivedAt,
|
|
244
|
+
from: String(envelope.mailFrom || deliveryCandidate.from || ''),
|
|
245
|
+
to: rcptTo,
|
|
246
|
+
receivedAtIso: new Date(receivedAt).toISOString(),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public async handleInboundPayload(
|
|
251
|
+
payloadArg: unknown,
|
|
252
|
+
handlerArg: TServiceMailInboundHandler,
|
|
253
|
+
): Promise<IServiceMailInboundHandleResult> {
|
|
254
|
+
const message = this.normalizeInboundMessage(payloadArg);
|
|
255
|
+
const result = await handlerArg(message);
|
|
256
|
+
return {
|
|
257
|
+
accepted: result?.accepted !== false,
|
|
258
|
+
workAppMessageId: result?.workAppMessageId,
|
|
259
|
+
message: result?.message,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public createInboundHandler(handlerArg: TServiceMailInboundHandler) {
|
|
264
|
+
return async (payloadArg: unknown) => await this.handleInboundPayload(payloadArg, handlerArg);
|
|
34
265
|
}
|
|
35
266
|
|
|
36
267
|
public async sendNotification(optionsArg: { toArg: string; subject: string; text: string }) {}
|
|
37
268
|
|
|
38
269
|
public async sendActionLink(optionsArg: { toArg: string; subject: string; text: string }) {}
|
|
270
|
+
|
|
271
|
+
private async readMailEnv(tokenArg?: string): Promise<{
|
|
272
|
+
from?: string;
|
|
273
|
+
smtpHost?: string;
|
|
274
|
+
smtpPort?: string;
|
|
275
|
+
smtpTlsMode?: string;
|
|
276
|
+
smtpUsername?: string;
|
|
277
|
+
smtpPassword?: string;
|
|
278
|
+
}> {
|
|
279
|
+
const prefix = tokenArg ? `MAIL_${tokenArg}_` : '';
|
|
280
|
+
return {
|
|
281
|
+
from: await this.readMailEnvVar(tokenArg ? `${prefix}FROM` : 'MAIL_FROM'),
|
|
282
|
+
smtpHost: await this.readMailEnvVar(`${prefix}SMTP_HOST`),
|
|
283
|
+
smtpPort: await this.readMailEnvVar(`${prefix}SMTP_PORT`),
|
|
284
|
+
smtpTlsMode: await this.readMailEnvVar(`${prefix}SMTP_TLS_MODE`),
|
|
285
|
+
smtpUsername: await this.readMailEnvVar(`${prefix}SMTP_USERNAME`),
|
|
286
|
+
smtpPassword: await this.readMailEnvVar(`${prefix}SMTP_PASSWORD`),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async readMailEnvVar(envNameArg: string): Promise<string | undefined> {
|
|
291
|
+
return await this.platformClientRef.getConfiguredEnvVar(envNameArg)
|
|
292
|
+
|| this.getCredentialEnvVarFromBindings(envNameArg);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private getCredentialEnvVarFromBindings(envNameArg: string): string | undefined {
|
|
296
|
+
for (const binding of this.platformClientRef.platformBindings || []) {
|
|
297
|
+
if (binding.desiredState === 'disabled' || binding.status === 'failed') continue;
|
|
298
|
+
if (binding.capability !== 'email') continue;
|
|
299
|
+
for (const credential of binding.credentials || []) {
|
|
300
|
+
const value = credential.env?.[envNameArg];
|
|
301
|
+
if (value) return value;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private hasAnyMailEnvValue(envArg?: {
|
|
307
|
+
from?: string;
|
|
308
|
+
smtpHost?: string;
|
|
309
|
+
smtpPort?: string;
|
|
310
|
+
smtpTlsMode?: string;
|
|
311
|
+
smtpUsername?: string;
|
|
312
|
+
smtpPassword?: string;
|
|
313
|
+
}): boolean {
|
|
314
|
+
return Boolean(envArg && Object.values(envArg).some(Boolean));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private normalizeTlsMode(valueArg: string): TServiceMailTlsMode {
|
|
318
|
+
if (valueArg === 'plain' || valueArg === 'starttls' || valueArg === 'implicitTls') return valueArg;
|
|
319
|
+
throw new Error(`Invalid SMTP_TLS_MODE: ${valueArg}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private getMailEnvToken(addressArg: string): string {
|
|
323
|
+
const token = addressArg.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').toUpperCase();
|
|
324
|
+
return /^\d/.test(token) ? `ADDR_${token}` : token;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private listToArray(valueArg: string | string[]): string[] {
|
|
328
|
+
return Array.isArray(valueArg) ? valueArg : [valueArg];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private normalizeAttachmentContent(contentArg: string | Uint8Array): string | Buffer {
|
|
332
|
+
return typeof contentArg === 'string' ? contentArg : Buffer.from(contentArg);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private asRecord(valueArg: unknown): Record<string, unknown> {
|
|
336
|
+
return valueArg && typeof valueArg === 'object' ? valueArg as Record<string, unknown> : {};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private normalizeHeaders(headersArg: unknown): Record<string, string | string[]> | undefined {
|
|
340
|
+
if (!headersArg || typeof headersArg !== 'object') return undefined;
|
|
341
|
+
const result: Record<string, string | string[]> = {};
|
|
342
|
+
for (const [key, value] of Object.entries(headersArg as Record<string, unknown>)) {
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
result[key] = value.map(String);
|
|
345
|
+
} else if (value !== undefined && value !== null) {
|
|
346
|
+
result[key] = String(value);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private extractHeader(rawMessageArg: string, headerNameArg: string): string | undefined {
|
|
353
|
+
const lowerName = `${headerNameArg.toLowerCase()}:`;
|
|
354
|
+
for (const line of rawMessageArg.split(/\r?\n/)) {
|
|
355
|
+
if (!line) return undefined;
|
|
356
|
+
if (line.toLowerCase().startsWith(lowerName)) {
|
|
357
|
+
return line.slice(lowerName.length).trim();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
39
361
|
}
|
|
@@ -25,7 +25,7 @@ export class SzPushNotificationConnector {
|
|
|
25
25
|
);
|
|
26
26
|
const response = await typedRequest.fire(optionsArg);
|
|
27
27
|
|
|
28
|
-
if (process.env
|
|
28
|
+
if (typeof process !== 'undefined' && process.env?.SERVEZONE_PLATFORMCLIENT_DEBUG) {
|
|
29
29
|
console.log('sendPushNotification response', response);
|
|
30
30
|
}
|
|
31
31
|
|
package/ts/index.ts
CHANGED