@apocaliss92/nodelink-js 0.4.26 → 0.4.28

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/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  NVR_HUB_EXACT_TYPES,
14
14
  NVR_HUB_MODEL_PATTERNS,
15
15
  ReolinkBaichuanApi,
16
+ _resetEmailPushBusForTests,
16
17
  abilitiesHasAny,
17
18
  asLogger,
18
19
  autoDetectDeviceType,
@@ -35,6 +36,7 @@ import {
35
36
  discoverViaTcpPortScan,
36
37
  discoverViaUdpBroadcast,
37
38
  discoverViaUdpDirect,
39
+ emitEmailPushEvent,
38
40
  encodeHeader,
39
41
  encodeMotionSensitivityListXml,
40
42
  encodeShelterCoord,
@@ -43,21 +45,27 @@ import {
43
45
  extractCanvasFromShelterXml,
44
46
  flattenAbilitiesForChannel,
45
47
  getConstructedVideoStreamOptions,
48
+ getEmailPushCameraResolver,
46
49
  getGlobalLogger,
50
+ getLastEmailPushEvent,
51
+ getRecentEmailPushEvents,
47
52
  getSupportItemForChannel,
48
53
  getVideoStream,
49
54
  isDualLenseModel,
50
55
  isNvrHubModel,
51
56
  isTcpFailureThatShouldFallbackToUdp,
57
+ mapEmailPushInferredType,
52
58
  maskUid,
53
59
  normalizeUid,
60
+ onEmailPushEvent,
54
61
  parseSupportXml,
55
62
  patchAiDetectCfgXml,
56
63
  patchMotionSensitivityListXml,
57
64
  patchShelterXml,
65
+ setEmailPushCameraResolver,
58
66
  setGlobalLogger,
59
67
  xmlIndicatesFloodlight
60
- } from "./chunk-F3XCYKYT.js";
68
+ } from "./chunk-NQ7D5TLR.js";
61
69
  import {
62
70
  ReolinkCgiApi,
63
71
  ReolinkHttpClient,
@@ -6003,9 +6011,9 @@ var BaichuanMjpegServer = class extends EventEmitter5 {
6003
6011
  this.started = true;
6004
6012
  const port = this.options.port ?? 8080;
6005
6013
  const host = this.options.host ?? "0.0.0.0";
6006
- const path2 = this.options.path ?? "/mjpeg";
6014
+ const path3 = this.options.path ?? "/mjpeg";
6007
6015
  this.httpServer = http5.createServer((req, res) => {
6008
- this.handleRequest(req, res, path2);
6016
+ this.handleRequest(req, res, path3);
6009
6017
  });
6010
6018
  return new Promise((resolve, reject) => {
6011
6019
  this.httpServer.on("error", (err) => {
@@ -6015,9 +6023,9 @@ var BaichuanMjpegServer = class extends EventEmitter5 {
6015
6023
  this.httpServer.listen(port, host, () => {
6016
6024
  this.log(
6017
6025
  "info",
6018
- `MJPEG server started on http://${host}:${port}${path2}`
6026
+ `MJPEG server started on http://${host}:${port}${path3}`
6019
6027
  );
6020
- this.emit("started", { host, port, path: path2 });
6028
+ this.emit("started", { host, port, path: path3 });
6021
6029
  resolve();
6022
6030
  });
6023
6031
  });
@@ -8274,6 +8282,272 @@ function base64DecodeToBytes(b64) {
8274
8282
  }
8275
8283
  return out.subarray(0, outIdx);
8276
8284
  }
8285
+
8286
+ // src/emailPush/server.ts
8287
+ import { SMTPServer } from "smtp-server";
8288
+ import { simpleParser } from "mailparser";
8289
+
8290
+ // src/emailPush/tls.ts
8291
+ import fs2 from "fs";
8292
+ import path2 from "path";
8293
+ async function loadEmailPushTls(params) {
8294
+ const certPath = path2.join(params.dir, "cert.pem");
8295
+ const keyPath = path2.join(params.dir, "key.pem");
8296
+ const warn = params.warn ?? ((m) => console.warn(`[email-push-tls] ${m}`));
8297
+ if (!fs2.existsSync(certPath) || !fs2.existsSync(keyPath)) {
8298
+ warn(
8299
+ `Email-push TLS requested but ${certPath} or ${keyPath} is missing. Place a PEM cert+key under that directory to enable STARTTLS, or disable TLS in the consumer config to silence this warning.`
8300
+ );
8301
+ return void 0;
8302
+ }
8303
+ return {
8304
+ cert: fs2.readFileSync(certPath),
8305
+ key: fs2.readFileSync(keyPath)
8306
+ };
8307
+ }
8308
+
8309
+ // src/emailPush/server.ts
8310
+ async function defaultLoadTls(dir, warn) {
8311
+ return loadEmailPushTls({ dir, warn });
8312
+ }
8313
+ function classifyMessage(parsed) {
8314
+ const haystack = `${parsed.subject ?? ""} ${parsed.text ?? ""}`.toLowerCase();
8315
+ if (/person|people|human/.test(haystack)) return "people";
8316
+ if (/vehicle|car|truck/.test(haystack)) return "vehicle";
8317
+ if (/dog[_\s-]?cat|pet|animal/.test(haystack)) return "animal";
8318
+ if (/face/.test(haystack)) return "face";
8319
+ if (/package|parcel/.test(haystack)) return "package";
8320
+ if (/doorbell|ring(?:ing)?\s+button/.test(haystack)) return "doorbell";
8321
+ if (/motion|alarm|alert|detect/.test(haystack)) return "motion";
8322
+ return "other";
8323
+ }
8324
+ function getCameraEmailAddress(cameraId, domain) {
8325
+ return `cam-${cameraId}@${domain}`;
8326
+ }
8327
+ function createEmailPushServer(params) {
8328
+ const log = {
8329
+ debug: params.logger?.debug ?? (() => {
8330
+ }),
8331
+ info: params.logger?.info ?? (() => {
8332
+ }),
8333
+ warn: params.logger?.warn ?? (() => {
8334
+ }),
8335
+ error: params.logger?.error ?? (() => {
8336
+ })
8337
+ };
8338
+ let config = params.config;
8339
+ let server;
8340
+ let status = buildInitialStatus(config);
8341
+ function parseRecipient(rcpt) {
8342
+ const [local, domain] = rcpt.toLowerCase().split("@");
8343
+ if (!local || !domain) return void 0;
8344
+ return { local, domain };
8345
+ }
8346
+ function resolveCameraIdFromRecipient(rcpt) {
8347
+ const parsed = parseRecipient(rcpt);
8348
+ if (!parsed) return void 0;
8349
+ if (parsed.domain !== config.domain.toLowerCase()) return void 0;
8350
+ const match = parsed.local.match(/^cam-(.+)$/);
8351
+ if (!match || !match[1]) return void 0;
8352
+ return params.cameraResolver(match[1]);
8353
+ }
8354
+ async function handleIncomingMessage(cameraId, recipient, raw) {
8355
+ let parsed;
8356
+ try {
8357
+ parsed = await simpleParser(raw);
8358
+ } catch (err) {
8359
+ log.warn(
8360
+ `Failed to parse mail for ${cameraId}: ${err instanceof Error ? err.message : err}`
8361
+ );
8362
+ status.messagesRejected++;
8363
+ return;
8364
+ }
8365
+ const receivedAtMs = Date.now();
8366
+ const event = {
8367
+ cameraId,
8368
+ recipient,
8369
+ inferredType: classifyMessage(parsed),
8370
+ receivedAtMs,
8371
+ subject: parsed.subject ?? "",
8372
+ from: typeof parsed.from === "object" && parsed.from !== null && "text" in parsed.from ? String(parsed.from.text) : "",
8373
+ bodyExcerpt: (parsed.text ?? "").slice(0, 500)
8374
+ };
8375
+ status.messagesAccepted++;
8376
+ log.info(
8377
+ `Email push for camera=${cameraId} type=${event.inferredType} subject="${event.subject.slice(0, 80)}"`
8378
+ );
8379
+ emitEmailPushEvent(event);
8380
+ }
8381
+ async function start() {
8382
+ if (server) {
8383
+ log.debug("startEmailPushServer called but server already running");
8384
+ return;
8385
+ }
8386
+ const tlsOptions = config.tls ? await (params.loadTls ?? defaultLoadTls)(
8387
+ config.tlsDir ?? "./email-push-tls",
8388
+ (m) => log.warn(m)
8389
+ ) : void 0;
8390
+ status = buildInitialStatus(config);
8391
+ const next = new SMTPServer({
8392
+ authOptional: !config.requireAuth,
8393
+ disabledCommands: config.requireAuth ? [] : ["AUTH"],
8394
+ allowInsecureAuth: !config.tls,
8395
+ size: config.maxMessageBytes,
8396
+ logger: {
8397
+ info: (...args) => log.debug(`[smtp] ${args.map((a) => String(a)).join(" ")}`),
8398
+ debug: (...args) => log.debug(`[smtp] ${args.map((a) => String(a)).join(" ")}`),
8399
+ error: (...args) => log.warn(`[smtp] ${args.map((a) => String(a)).join(" ")}`)
8400
+ },
8401
+ ...tlsOptions ? { secure: false, ...tlsOptions } : {},
8402
+ onConnect(session, callback) {
8403
+ log.info(
8404
+ `SMTP connect from ${session.remoteAddress} (sessionId=${session.id})`
8405
+ );
8406
+ callback();
8407
+ },
8408
+ onClose(session) {
8409
+ log.debug(
8410
+ `SMTP close ${session.remoteAddress} (sessionId=${session.id})`
8411
+ );
8412
+ },
8413
+ onMailFrom(address, session, callback) {
8414
+ log.info(
8415
+ `SMTP MAIL FROM ${address.address} (sessionId=${session.id})`
8416
+ );
8417
+ callback();
8418
+ },
8419
+ onAuth(auth, session, callback) {
8420
+ const expectedUser = config.authUsername;
8421
+ const expectedPass = config.authPassword;
8422
+ if (!expectedUser || !expectedPass) {
8423
+ log.warn(
8424
+ `SMTP AUTH rejected from ${session.remoteAddress} (sessionId=${session.id}): server has no credentials configured`
8425
+ );
8426
+ return callback(new Error("Email push auth not configured"));
8427
+ }
8428
+ const stripDomain = (u) => {
8429
+ const at = u.lastIndexOf("@");
8430
+ if (at < 0) return u;
8431
+ const local = u.slice(0, at);
8432
+ const domain = u.slice(at + 1).toLowerCase();
8433
+ return domain === config.domain.toLowerCase() ? local : u;
8434
+ };
8435
+ const triedUserNorm = stripDomain(auth.username ?? "");
8436
+ const expectedUserNorm = stripDomain(expectedUser);
8437
+ if (triedUserNorm === expectedUserNorm && auth.password === expectedPass) {
8438
+ log.info(
8439
+ `SMTP AUTH ok method=${auth.method} user=${auth.username} (sessionId=${session.id})`
8440
+ );
8441
+ return callback(null, { user: auth.username });
8442
+ }
8443
+ log.warn(
8444
+ `SMTP AUTH FAIL method=${auth.method} from=${session.remoteAddress} triedUser="${auth.username}" expectedUser="${expectedUser}" triedPasswordLen=${auth.password?.length ?? 0} (sessionId=${session.id})`
8445
+ );
8446
+ return callback(new Error("Invalid username or password"));
8447
+ },
8448
+ onRcptTo(address, _session, callback) {
8449
+ const cameraId = resolveCameraIdFromRecipient(address.address);
8450
+ if (!cameraId) {
8451
+ status.messagesRejected++;
8452
+ return callback(
8453
+ new Error(
8454
+ `Unknown recipient ${address.address} (not registered)`
8455
+ )
8456
+ );
8457
+ }
8458
+ callback();
8459
+ },
8460
+ onData(stream, session, callback) {
8461
+ const chunks = [];
8462
+ stream.on("data", (chunk) => chunks.push(chunk));
8463
+ stream.on("end", () => {
8464
+ const recipients = session.envelope.rcptTo?.map((r) => r.address) ?? [];
8465
+ const buffer = Buffer.concat(chunks);
8466
+ const matched = recipients.map((r) => ({
8467
+ recipient: r,
8468
+ cameraId: resolveCameraIdFromRecipient(r)
8469
+ })).filter(
8470
+ (m) => Boolean(m.cameraId)
8471
+ );
8472
+ if (matched.length === 0) {
8473
+ status.messagesRejected++;
8474
+ return callback(new Error("No recognised recipients"));
8475
+ }
8476
+ Promise.all(
8477
+ matched.map(
8478
+ (m) => handleIncomingMessage(m.cameraId, m.recipient, buffer)
8479
+ )
8480
+ ).then(() => callback()).catch((err) => {
8481
+ const msg = err instanceof Error ? err.message : String(err);
8482
+ log.error(`Email push pipeline error: ${msg}`);
8483
+ status.lastErrorMessage = msg;
8484
+ callback(new Error(msg));
8485
+ });
8486
+ });
8487
+ stream.on("error", (err) => {
8488
+ log.warn(`SMTP stream error: ${err.message}`);
8489
+ callback(err);
8490
+ });
8491
+ }
8492
+ });
8493
+ next.on("error", (err) => {
8494
+ status.lastErrorMessage = err.message;
8495
+ log.error(`Email push server error: ${err.message}`);
8496
+ });
8497
+ await new Promise((resolve, reject) => {
8498
+ next.listen(config.port, config.bindHost, () => {
8499
+ status.running = true;
8500
+ status.startedAtMs = Date.now();
8501
+ log.info(
8502
+ `Email push SMTP listening on ${config.bindHost}:${config.port} (domain=${config.domain}, auth=${config.requireAuth}, tls=${config.tls})`
8503
+ );
8504
+ resolve();
8505
+ });
8506
+ next.once("error", reject);
8507
+ });
8508
+ server = next;
8509
+ }
8510
+ async function stop() {
8511
+ if (!server) return;
8512
+ const active = server;
8513
+ server = void 0;
8514
+ await new Promise((resolve) => {
8515
+ active.close(() => resolve());
8516
+ });
8517
+ status.running = false;
8518
+ log.info("Email push SMTP server stopped");
8519
+ }
8520
+ async function restart() {
8521
+ await stop();
8522
+ await start();
8523
+ }
8524
+ return {
8525
+ start,
8526
+ stop,
8527
+ restart,
8528
+ getStatus() {
8529
+ return { ...status };
8530
+ },
8531
+ updateConfig(next) {
8532
+ config = next;
8533
+ }
8534
+ };
8535
+ }
8536
+ function buildInitialStatus(config) {
8537
+ return {
8538
+ enabled: true,
8539
+ running: false,
8540
+ port: config.port,
8541
+ bindHost: config.bindHost,
8542
+ domain: config.domain,
8543
+ requireAuth: config.requireAuth,
8544
+ tls: config.tls,
8545
+ messagesAccepted: 0,
8546
+ messagesRejected: 0,
8547
+ startedAtMs: void 0,
8548
+ lastErrorMessage: void 0
8549
+ };
8550
+ }
8277
8551
  export {
8278
8552
  AesStreamDecryptor,
8279
8553
  AutodiscoveryClient,
@@ -8446,6 +8720,7 @@ export {
8446
8720
  ReolinkCgiApi,
8447
8721
  ReolinkHttpClient,
8448
8722
  Rfc4571Muxer,
8723
+ _resetEmailPushBusForTests,
8449
8724
  abilitiesHasAny,
8450
8725
  aesDecrypt,
8451
8726
  aesEncrypt,
@@ -8491,6 +8766,7 @@ export {
8491
8766
  createBaichuanEndpointsServer,
8492
8767
  createDebugGateLogger,
8493
8768
  createDiagnosticsBundle,
8769
+ createEmailPushServer,
8494
8770
  createLogger,
8495
8771
  createMjpegBoundary,
8496
8772
  createNativeStream,
@@ -8518,6 +8794,7 @@ export {
8518
8794
  discoverViaTcpPortScan,
8519
8795
  discoverViaUdpBroadcast,
8520
8796
  discoverViaUdpDirect,
8797
+ emitEmailPushEvent,
8521
8798
  encodeHeader,
8522
8799
  encodeMotionScopeBitmap,
8523
8800
  encodeMotionSensitivityListXml,
@@ -8534,10 +8811,14 @@ export {
8534
8811
  flattenAbilitiesForChannel,
8535
8812
  formatMjpegFrame,
8536
8813
  fullCoverageScope,
8814
+ getCameraEmailAddress,
8537
8815
  getConstructedVideoStreamOptions,
8816
+ getEmailPushCameraResolver,
8538
8817
  getGlobalLogger,
8539
8818
  getH265NalType,
8819
+ getLastEmailPushEvent,
8540
8820
  getMjpegContentType,
8821
+ getRecentEmailPushEvents,
8541
8822
  getSupportItemForChannel,
8542
8823
  getVideoStream,
8543
8824
  getVideoclipClientInfo,
@@ -8552,12 +8833,15 @@ export {
8552
8833
  isTcpFailureThatShouldFallbackToUdp,
8553
8834
  isValidH264AnnexBAccessUnit,
8554
8835
  isValidH265AnnexBAccessUnit,
8836
+ loadEmailPushTls,
8837
+ mapEmailPushInferredType,
8555
8838
  maskUid,
8556
8839
  md5HexUpper,
8557
8840
  md5StrModern,
8558
8841
  normalizeDayNightMode,
8559
8842
  normalizeOpenClose,
8560
8843
  normalizeUid,
8844
+ onEmailPushEvent,
8561
8845
  packetizeAacAdtsFrame,
8562
8846
  packetizeAacRawFrame,
8563
8847
  packetizeH264,
@@ -8575,6 +8859,7 @@ export {
8575
8859
  runMultifocalDiagnosticsConsecutively,
8576
8860
  sampleStreams,
8577
8861
  sanitizeFixtureData,
8862
+ setEmailPushCameraResolver,
8578
8863
  setGlobalLogger,
8579
8864
  splitAnnexBToNalPayloads,
8580
8865
  splitAnnexBToNals,