@apocaliss92/nodelink-js 0.4.24 → 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.cjs CHANGED
@@ -3293,10 +3293,10 @@ function buildRtspPath(channel, stream) {
3293
3293
  }
3294
3294
  function buildRtspUrl(params) {
3295
3295
  const port = params.port ?? 554;
3296
- const path6 = buildRtspPath(params.channel, params.stream);
3296
+ const path7 = buildRtspPath(params.channel, params.stream);
3297
3297
  const user = encodeURIComponent(params.username);
3298
3298
  const pass = encodeURIComponent(params.password);
3299
- return `rtsp://${user}:${pass}@${params.host}:${port}${path6}`;
3299
+ return `rtsp://${user}:${pass}@${params.host}:${port}${path7}`;
3300
3300
  }
3301
3301
  var init_urls = __esm({
3302
3302
  "src/rtsp/urls.ts"() {
@@ -4845,8 +4845,8 @@ async function runMultifocalDiagnosticsConsecutively(params) {
4845
4845
  for (const app of rtmpApps) {
4846
4846
  for (const streamName of streams) {
4847
4847
  const streamType = streamName.includes("sub") || streamName === "sub" || streamName === "mobile" ? 1 : 0;
4848
- const path6 = `/${app}/channel${params.channel}_${streamName}.bcs`;
4849
- const u = new URL(`rtmp://${params.host}:1935${path6}`);
4848
+ const path7 = `/${app}/channel${params.channel}_${streamName}.bcs`;
4849
+ const u = new URL(`rtmp://${params.host}:1935${path7}`);
4850
4850
  u.searchParams.set("channel", params.channel.toString());
4851
4851
  u.searchParams.set("stream", streamType.toString());
4852
4852
  u.searchParams.set("user", params.username);
@@ -8334,6 +8334,7 @@ __export(index_exports, {
8334
8334
  ReolinkCgiApi: () => ReolinkCgiApi,
8335
8335
  ReolinkHttpClient: () => ReolinkHttpClient,
8336
8336
  Rfc4571Muxer: () => Rfc4571Muxer,
8337
+ _resetEmailPushBusForTests: () => _resetEmailPushBusForTests,
8337
8338
  abilitiesHasAny: () => abilitiesHasAny,
8338
8339
  aesDecrypt: () => aesDecrypt,
8339
8340
  aesEncrypt: () => aesEncrypt,
@@ -8379,6 +8380,7 @@ __export(index_exports, {
8379
8380
  createBaichuanEndpointsServer: () => createBaichuanEndpointsServer,
8380
8381
  createDebugGateLogger: () => createDebugGateLogger,
8381
8382
  createDiagnosticsBundle: () => createDiagnosticsBundle,
8383
+ createEmailPushServer: () => createEmailPushServer,
8382
8384
  createLogger: () => createLogger,
8383
8385
  createMjpegBoundary: () => createMjpegBoundary,
8384
8386
  createNativeStream: () => createNativeStream,
@@ -8406,6 +8408,7 @@ __export(index_exports, {
8406
8408
  discoverViaTcpPortScan: () => discoverViaTcpPortScan,
8407
8409
  discoverViaUdpBroadcast: () => discoverViaUdpBroadcast,
8408
8410
  discoverViaUdpDirect: () => discoverViaUdpDirect,
8411
+ emitEmailPushEvent: () => emitEmailPushEvent,
8409
8412
  encodeHeader: () => encodeHeader,
8410
8413
  encodeMotionScopeBitmap: () => encodeMotionScopeBitmap,
8411
8414
  encodeMotionSensitivityListXml: () => encodeMotionSensitivityListXml,
@@ -8422,10 +8425,14 @@ __export(index_exports, {
8422
8425
  flattenAbilitiesForChannel: () => flattenAbilitiesForChannel,
8423
8426
  formatMjpegFrame: () => formatMjpegFrame,
8424
8427
  fullCoverageScope: () => fullCoverageScope,
8428
+ getCameraEmailAddress: () => getCameraEmailAddress,
8425
8429
  getConstructedVideoStreamOptions: () => getConstructedVideoStreamOptions,
8430
+ getEmailPushCameraResolver: () => getEmailPushCameraResolver,
8426
8431
  getGlobalLogger: () => getGlobalLogger,
8427
8432
  getH265NalType: () => getH265NalType,
8433
+ getLastEmailPushEvent: () => getLastEmailPushEvent,
8428
8434
  getMjpegContentType: () => getMjpegContentType,
8435
+ getRecentEmailPushEvents: () => getRecentEmailPushEvents,
8429
8436
  getSupportItemForChannel: () => getSupportItemForChannel,
8430
8437
  getVideoStream: () => getVideoStream,
8431
8438
  getVideoclipClientInfo: () => getVideoclipClientInfo,
@@ -8440,12 +8447,15 @@ __export(index_exports, {
8440
8447
  isTcpFailureThatShouldFallbackToUdp: () => isTcpFailureThatShouldFallbackToUdp,
8441
8448
  isValidH264AnnexBAccessUnit: () => isValidH264AnnexBAccessUnit,
8442
8449
  isValidH265AnnexBAccessUnit: () => isValidH265AnnexBAccessUnit,
8450
+ loadEmailPushTls: () => loadEmailPushTls,
8451
+ mapEmailPushInferredType: () => mapEmailPushInferredType,
8443
8452
  maskUid: () => maskUid,
8444
8453
  md5HexUpper: () => md5HexUpper,
8445
8454
  md5StrModern: () => md5StrModern,
8446
8455
  normalizeDayNightMode: () => normalizeDayNightMode,
8447
8456
  normalizeOpenClose: () => normalizeOpenClose,
8448
8457
  normalizeUid: () => normalizeUid,
8458
+ onEmailPushEvent: () => onEmailPushEvent,
8449
8459
  packetizeAacAdtsFrame: () => packetizeAacAdtsFrame,
8450
8460
  packetizeAacRawFrame: () => packetizeAacRawFrame,
8451
8461
  packetizeH264: () => packetizeH264,
@@ -8463,6 +8473,7 @@ __export(index_exports, {
8463
8473
  runMultifocalDiagnosticsConsecutively: () => runMultifocalDiagnosticsConsecutively,
8464
8474
  sampleStreams: () => sampleStreams,
8465
8475
  sanitizeFixtureData: () => sanitizeFixtureData,
8476
+ setEmailPushCameraResolver: () => setEmailPushCameraResolver,
8466
8477
  setGlobalLogger: () => setGlobalLogger,
8467
8478
  splitAnnexBToNalPayloads: () => splitAnnexBToNalPayloads,
8468
8479
  splitAnnexBToNals: () => splitAnnexBToNals,
@@ -13646,6 +13657,21 @@ async function* createNativeStream(api, channel, profile, options) {
13646
13657
  let audioSampleRate = null;
13647
13658
  let streamStarted = false;
13648
13659
  let closed = false;
13660
+ const signal = options?.signal;
13661
+ let sleepResolve = null;
13662
+ let sleepTimer = null;
13663
+ const clearSleepTimer = () => {
13664
+ if (sleepTimer) {
13665
+ clearTimeout(sleepTimer);
13666
+ sleepTimer = null;
13667
+ }
13668
+ };
13669
+ const handleAbort = () => {
13670
+ clearSleepTimer();
13671
+ const r = sleepResolve;
13672
+ sleepResolve = null;
13673
+ r?.();
13674
+ };
13649
13675
  const onError = (_error) => {
13650
13676
  closed = true;
13651
13677
  api.logger?.warn?.(
@@ -13743,7 +13769,13 @@ async function* createNativeStream(api, channel, profile, options) {
13743
13769
  }
13744
13770
  });
13745
13771
  streamStarted = true;
13746
- const signal = options?.signal;
13772
+ if (signal) {
13773
+ if (signal.aborted) {
13774
+ closed = true;
13775
+ } else {
13776
+ signal.addEventListener("abort", handleAbort);
13777
+ }
13778
+ }
13747
13779
  while (!closed && !signal?.aborted) {
13748
13780
  if (frameQueue.length > 0) {
13749
13781
  const frame = frameQueue.shift();
@@ -13751,31 +13783,30 @@ async function* createNativeStream(api, channel, profile, options) {
13751
13783
  } else {
13752
13784
  await new Promise((resolve) => {
13753
13785
  frameResolve = resolve;
13754
- const timer = setTimeout(() => {
13786
+ sleepResolve = resolve;
13787
+ sleepTimer = setTimeout(() => {
13788
+ sleepTimer = null;
13789
+ if (sleepResolve === resolve) sleepResolve = null;
13755
13790
  if (frameResolve === resolve) {
13756
13791
  frameResolve = null;
13757
13792
  resolve();
13758
13793
  }
13759
13794
  }, 1e3);
13760
- if (signal) {
13761
- const onAbort = () => {
13762
- clearTimeout(timer);
13763
- if (frameResolve === resolve) frameResolve = null;
13764
- resolve();
13765
- };
13766
- if (signal.aborted) {
13767
- clearTimeout(timer);
13768
- frameResolve = null;
13769
- resolve();
13770
- } else {
13771
- signal.addEventListener("abort", onAbort, { once: true });
13772
- }
13795
+ if (signal?.aborted) {
13796
+ clearSleepTimer();
13797
+ sleepResolve = null;
13798
+ frameResolve = null;
13799
+ resolve();
13773
13800
  }
13774
13801
  });
13802
+ sleepResolve = null;
13803
+ clearSleepTimer();
13775
13804
  }
13776
13805
  }
13777
13806
  } finally {
13778
13807
  closed = true;
13808
+ if (signal) signal.removeEventListener("abort", handleAbort);
13809
+ clearSleepTimer();
13779
13810
  try {
13780
13811
  await videoStream.stop();
13781
13812
  } catch {
@@ -15809,12 +15840,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15809
15840
  if (frame.videoType === "H264" || frame.videoType === "H265") {
15810
15841
  this.setFlowVideoType(frame.videoType, "native stream");
15811
15842
  }
15812
- this.flow.extractParameterSets(frame.data);
15813
- const { hasParamSets: hasParamSets2 } = this.flow.getFmtp();
15814
- if (hasParamSets2) {
15843
+ if (!this.flow.getFmtp().hasParamSets) {
15844
+ this.flow.extractParameterSets(frame.data);
15845
+ }
15846
+ if (this.flow.getFmtp().hasParamSets) {
15815
15847
  this.markFirstFrameReceived();
15816
15848
  }
15817
- const isKeyframe = this.isRawFrameKeyframe(frame);
15849
+ const isKeyframe = typeof frame.isKeyframe === "boolean" ? frame.isKeyframe : this.isRawFrameKeyframe(frame);
15818
15850
  this.prebuffer.push({
15819
15851
  frame: { ...frame, data: Buffer.from(frame.data) },
15820
15852
  time: Date.now(),
@@ -19635,6 +19667,93 @@ var applyFloodlightSettingsToXml = (xml, settings) => {
19635
19667
  return modifiedXml;
19636
19668
  };
19637
19669
 
19670
+ // src/reolink/baichuan/utils/whiteLedStatusPush.ts
19671
+ var parseFloodlightStatusListPushXml = (xml) => {
19672
+ const out = [];
19673
+ const re = /<channel>\s*(\d+)\s*<\/channel>[\s\S]*?<status>\s*(\d+)\s*<\/status>/gi;
19674
+ let m;
19675
+ while ((m = re.exec(xml)) !== null) {
19676
+ const channel = Number.parseInt(m[1] ?? "", 10);
19677
+ const status = Number.parseInt(m[2] ?? "", 10);
19678
+ if (!Number.isFinite(channel) || !Number.isFinite(status)) continue;
19679
+ out.push({ channel, status });
19680
+ }
19681
+ return out;
19682
+ };
19683
+
19684
+ // src/reolink/baichuan/utils/sirenStatusPush.ts
19685
+ var parseSirenStatusListPushXml = (xml) => {
19686
+ const out = [];
19687
+ const re = /<(?:channelId|channel)>\s*(\d+)\s*<\/(?:channelId|channel)>[\s\S]*?<status>\s*(\d+)\s*<\/status>(?:[\s\S]*?<playing>\s*(\d+)\s*<\/playing>)?/gi;
19688
+ let m;
19689
+ while ((m = re.exec(xml)) !== null) {
19690
+ const channel = Number.parseInt(m[1] ?? "", 10);
19691
+ const status = Number.parseInt(m[2] ?? "", 10);
19692
+ if (!Number.isFinite(channel) || !Number.isFinite(status)) continue;
19693
+ const entry = { channel, status };
19694
+ if (m[3] !== void 0) {
19695
+ const playing = Number.parseInt(m[3], 10);
19696
+ if (Number.isFinite(playing)) entry.playing = playing;
19697
+ }
19698
+ out.push(entry);
19699
+ }
19700
+ return out;
19701
+ };
19702
+
19703
+ // src/emailPush/bus.ts
19704
+ var import_node_events5 = require("events");
19705
+ var emitter = new import_node_events5.EventEmitter();
19706
+ var cameraResolver = () => void 0;
19707
+ var lastEventByCamera = /* @__PURE__ */ new Map();
19708
+ var MAX_GLOBAL_EVENTS = 300;
19709
+ var globalRecentEvents = [];
19710
+ function setEmailPushCameraResolver(resolver) {
19711
+ cameraResolver = resolver;
19712
+ }
19713
+ function getEmailPushCameraResolver() {
19714
+ return cameraResolver;
19715
+ }
19716
+ function onEmailPushEvent(handler) {
19717
+ emitter.on("event", handler);
19718
+ return () => emitter.off("event", handler);
19719
+ }
19720
+ function emitEmailPushEvent(event) {
19721
+ lastEventByCamera.set(event.cameraId, event);
19722
+ globalRecentEvents.unshift(event);
19723
+ if (globalRecentEvents.length > MAX_GLOBAL_EVENTS) {
19724
+ globalRecentEvents.length = MAX_GLOBAL_EVENTS;
19725
+ }
19726
+ emitter.emit("event", event);
19727
+ }
19728
+ function getLastEmailPushEvent(cameraId) {
19729
+ return lastEventByCamera.get(cameraId);
19730
+ }
19731
+ function getRecentEmailPushEvents(limit = MAX_GLOBAL_EVENTS) {
19732
+ const clamped = Math.max(0, Math.min(limit, MAX_GLOBAL_EVENTS));
19733
+ return globalRecentEvents.slice(0, clamped);
19734
+ }
19735
+ function mapEmailPushInferredType(inferred) {
19736
+ switch (inferred) {
19737
+ case "motion":
19738
+ case "doorbell":
19739
+ case "people":
19740
+ case "vehicle":
19741
+ case "animal":
19742
+ case "face":
19743
+ case "package":
19744
+ return inferred;
19745
+ case "other":
19746
+ default:
19747
+ return "motion";
19748
+ }
19749
+ }
19750
+ function _resetEmailPushBusForTests() {
19751
+ emitter.removeAllListeners();
19752
+ cameraResolver = () => void 0;
19753
+ lastEventByCamera.clear();
19754
+ globalRecentEvents.length = 0;
19755
+ }
19756
+
19638
19757
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
19639
19758
  var DUAL_LENS_DUAL_MOTION_MODELS = /* @__PURE__ */ new Set([
19640
19759
  "Reolink Duo PoE",
@@ -19780,7 +19899,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19780
19899
  * general socket is created, logged in, and all event/push/guard listeners
19781
19900
  * are re-attached automatically.
19782
19901
  *
19783
- * This is a **no-op** when the API is already {@link isReady}.
19902
+ * This is a **no-op** when the API is already ready (see `isReadyState()`).
19784
19903
  *
19785
19904
  * @throws If `close()` was called — the API is permanently closed and a new
19786
19905
  * instance must be created.
@@ -19861,7 +19980,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19861
19980
  /**
19862
19981
  * Attach event, push, channelInfo, and guard listeners to the current
19863
19982
  * "general" client. Called from the constructor and from
19864
- * {@link reconnectGeneralSocket}.
19983
+ * `reconnectGeneralSocket()`.
19865
19984
  */
19866
19985
  setupGeneralClientListeners() {
19867
19986
  const client = this.client;
@@ -19913,7 +20032,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19913
20032
  });
19914
20033
  client.on("push", (frame) => {
19915
20034
  const cmdId = frame.header.cmdId;
19916
- if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
20035
+ if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST && cmdId !== BC_CMD_ID_FLOODLIGHT_STATUS_LIST && cmdId !== BC_CMD_ID_GET_AUDIO_ALARM) {
19917
20036
  return;
19918
20037
  }
19919
20038
  try {
@@ -21450,6 +21569,40 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
21450
21569
  this.econnresetStormRebootInFlight = void 0;
21451
21570
  });
21452
21571
  }
21572
+ /**
21573
+ * Bind this API instance to the global email-push bus so that incoming
21574
+ * SMTP-delivered motion / AI events for the matching camera surface on
21575
+ * this instance's standard `onSimpleEvent` channel. The consumer keeps
21576
+ * a single subscription (`onSimpleEvent`) and gets both the native
21577
+ * Baichuan push and the email-push transport on the same stream.
21578
+ *
21579
+ * - `cameraId` shorthand: match events with `event.cameraId === cameraId`.
21580
+ * - `match`: arbitrary predicate (e.g. when the consumer uses a
21581
+ * nickname-based mapping or wants to handle multiple recipients).
21582
+ *
21583
+ * Returns an `off()` handle. Safe to call repeatedly — each call
21584
+ * registers its own listener.
21585
+ */
21586
+ subscribeEmailPushEvents(params) {
21587
+ const channel = params.channel ?? 0;
21588
+ const matches = "match" in params ? params.match : (event) => event.cameraId === params.cameraId;
21589
+ const off = onEmailPushEvent((event) => {
21590
+ if (!matches(event)) return;
21591
+ this.dispatchSimpleEvent({
21592
+ type: mapEmailPushInferredType(event.inferredType),
21593
+ channel,
21594
+ timestamp: event.receivedAtMs
21595
+ });
21596
+ if (event.inferredType !== "motion" && event.inferredType !== "doorbell" && event.inferredType !== "other") {
21597
+ this.dispatchSimpleEvent({
21598
+ type: "motion",
21599
+ channel,
21600
+ timestamp: event.receivedAtMs
21601
+ });
21602
+ }
21603
+ });
21604
+ return off;
21605
+ }
21453
21606
  /**
21454
21607
  * Subscribe to minimal high-level events.
21455
21608
  * The API manages Baichuan subscribe/unsubscribe automatically.
@@ -21508,7 +21661,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
21508
21661
  * Subscribe to per-frame detection events sourced from the BcMedia
21509
21662
  * `additionalHeader` block on active video streams.
21510
21663
  *
21511
- * Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
21664
+ * Mirrors `onSimpleEvent()` but is fed by the streaming side-channel:
21512
21665
  * one event fires for every I-frame / P-frame that carries an overlay block.
21513
21666
  * Coordinates are reported in normalized [0, 1] fractions of the source
21514
21667
  * frame, so the same box renders correctly on mainStream, subStream, and
@@ -21535,7 +21688,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
21535
21688
  * Subscribe to AI object detections (people / vehicle / animal / face boxes
21536
21689
  * with class label and confidence) without managing a video stream yourself.
21537
21690
  *
21538
- * Mirrors {@link onSimpleEvent} end-to-end: on the first listener for a given
21691
+ * Mirrors `onSimpleEvent()` end-to-end: on the first listener for a given
21539
21692
  * `(channel, profile)` tuple the API ensures the corresponding video stream
21540
21693
  * is running (the pool socket may already be shared with a regular consumer),
21541
21694
  * forwards every box-bearing `additionalHeader` to your callback, and tears
@@ -25355,24 +25508,24 @@ ${stderr}`)
25355
25508
  async muxToMp4(params) {
25356
25509
  const { spawn: spawn13 } = await import("child_process");
25357
25510
  const { randomUUID: randomUUID3 } = await import("crypto");
25358
- const fs6 = await import("fs/promises");
25511
+ const fs7 = await import("fs/promises");
25359
25512
  const os2 = await import("os");
25360
- const path6 = await import("path");
25513
+ const path7 = await import("path");
25361
25514
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
25362
25515
  const tmpDir = os2.tmpdir();
25363
25516
  const id = randomUUID3();
25364
25517
  const videoFormat = params.videoCodec === "H265" ? "hevc" : "h264";
25365
- const videoPath = path6.join(tmpDir, `reolink-${id}.${videoFormat}`);
25366
- const outputPath = path6.join(tmpDir, `reolink-${id}.mp4`);
25518
+ const videoPath = path7.join(tmpDir, `reolink-${id}.${videoFormat}`);
25519
+ const outputPath = path7.join(tmpDir, `reolink-${id}.mp4`);
25367
25520
  let audioPath = null;
25368
25521
  if (params.audioData && params.audioData.length > 0 && params.audioCodec) {
25369
25522
  const audioExt = params.audioCodec === "Aac" ? "aac" : "raw";
25370
- audioPath = path6.join(tmpDir, `reolink-${id}.${audioExt}`);
25523
+ audioPath = path7.join(tmpDir, `reolink-${id}.${audioExt}`);
25371
25524
  }
25372
25525
  try {
25373
- await fs6.writeFile(videoPath, params.videoData);
25526
+ await fs7.writeFile(videoPath, params.videoData);
25374
25527
  if (audioPath && params.audioData) {
25375
- await fs6.writeFile(audioPath, params.audioData);
25528
+ await fs7.writeFile(audioPath, params.audioData);
25376
25529
  }
25377
25530
  const args = ["-hide_banner", "-loglevel", "error", "-y"];
25378
25531
  if (params.fps > 0) {
@@ -25425,13 +25578,13 @@ ${stderr}`)
25425
25578
  }
25426
25579
  });
25427
25580
  });
25428
- return await fs6.readFile(outputPath);
25581
+ return await fs7.readFile(outputPath);
25429
25582
  } finally {
25430
- await fs6.unlink(videoPath).catch(() => {
25583
+ await fs7.unlink(videoPath).catch(() => {
25431
25584
  });
25432
- if (audioPath) await fs6.unlink(audioPath).catch(() => {
25585
+ if (audioPath) await fs7.unlink(audioPath).catch(() => {
25433
25586
  });
25434
- await fs6.unlink(outputPath).catch(() => {
25587
+ await fs7.unlink(outputPath).catch(() => {
25435
25588
  });
25436
25589
  }
25437
25590
  }
@@ -28379,7 +28532,7 @@ ${xml}`
28379
28532
  * Field meaning per stream:
28380
28533
  * - `audio` — 0/1 toggle
28381
28534
  * - `width`/`height` — resolution in pixels. Must be one of the
28382
- * resolutions returned by {@link getStreamInfoList}.
28535
+ * resolutions returned by `getStreamInfoList()`.
28383
28536
  * - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
28384
28537
  * - `frameRate` — fps. Must match the table from `getStreamInfoList`.
28385
28538
  * - `videoEncType` — `"h264"` or `"h265"`
@@ -28954,6 +29107,33 @@ ${xml}`
28954
29107
  };
28955
29108
  return;
28956
29109
  }
29110
+ if (cmdId === BC_CMD_ID_FLOODLIGHT_STATUS_LIST) {
29111
+ const entries = parseFloodlightStatusListPushXml(xml);
29112
+ if (entries.length === 0) return;
29113
+ for (const entry of entries) {
29114
+ const channel = normalizePushChannel(entry.channel) ?? channelFromHeader;
29115
+ getEntry(channel).floodlightStatus = {
29116
+ updatedAtMs: now,
29117
+ value: { status: entry.status === 1 }
29118
+ };
29119
+ }
29120
+ return;
29121
+ }
29122
+ if (cmdId === BC_CMD_ID_GET_AUDIO_ALARM) {
29123
+ const entries = parseSirenStatusListPushXml(xml);
29124
+ if (entries.length === 0) return;
29125
+ for (const entry of entries) {
29126
+ const channel = normalizePushChannel(entry.channel) ?? channelFromHeader;
29127
+ getEntry(channel).sirenStatus = {
29128
+ updatedAtMs: now,
29129
+ value: {
29130
+ status: entry.status === 1,
29131
+ ...entry.playing !== void 0 ? { playing: entry.playing === 1 } : {}
29132
+ }
29133
+ };
29134
+ }
29135
+ return;
29136
+ }
28957
29137
  }
28958
29138
  /** Read-only snapshot of cached settings pushes (cmd_id 78/79/464/484/623/723). */
28959
29139
  getSettingsPushCacheSnapshot() {
@@ -28986,6 +29166,18 @@ ${xml}`
28986
29166
  ...entry.coordinatePointList,
28987
29167
  value: { ...entry.coordinatePointList.value }
28988
29168
  }
29169
+ } : {},
29170
+ ...entry.floodlightStatus ? {
29171
+ floodlightStatus: {
29172
+ ...entry.floodlightStatus,
29173
+ value: { ...entry.floodlightStatus.value }
29174
+ }
29175
+ } : {},
29176
+ ...entry.sirenStatus ? {
29177
+ sirenStatus: {
29178
+ ...entry.sirenStatus,
29179
+ value: { ...entry.sirenStatus.value }
29180
+ }
28989
29181
  } : {}
28990
29182
  });
28991
29183
  }
@@ -29009,6 +29201,29 @@ ${xml}`
29009
29201
  getCoordinatePointListFromPushCache(channel = 0) {
29010
29202
  return this.settingsPushCache.get(channel)?.coordinatePointList;
29011
29203
  }
29204
+ /**
29205
+ * Last cmd_id 291 (FloodlightStatusList) push observed for the channel.
29206
+ * The camera emits this whenever the floodlight transitions on/off,
29207
+ * including the auto-off after the FloodlightManual duration. This is
29208
+ * the only reliable source for the current manual state because cmd 289
29209
+ * only returns the FloodlightTask config.
29210
+ *
29211
+ * Returns undefined when no push has been received yet.
29212
+ */
29213
+ getCachedFloodlightStatus(channel = 0) {
29214
+ return this.settingsPushCache.get(channel)?.floodlightStatus;
29215
+ }
29216
+ /**
29217
+ * Last cmd_id 547 (SirenStatusList) push observed for the channel.
29218
+ * Captures the actual on/off transitions including the firmware's
29219
+ * built-in auto-off after the siren playback duration expires —
29220
+ * polling cmd 547 alone can race that auto-off.
29221
+ *
29222
+ * Returns undefined when no push has been received yet.
29223
+ */
29224
+ getCachedSirenStatus(channel = 0) {
29225
+ return this.settingsPushCache.get(channel)?.sirenStatus;
29226
+ }
29012
29227
  // --------------------
29013
29228
  // PCAP-derived settings getters (typed wrappers)
29014
29229
  // --------------------
@@ -29706,10 +29921,12 @@ ${xml}`
29706
29921
  const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
29707
29922
  const attachmentType = params.attachmentType ?? "picture";
29708
29923
  const interval = params.interval ?? 30;
29924
+ const rawUser = params.authUsername ?? recipient;
29925
+ const wireUser = rawUser.includes("@") ? rawUser : `${rawUser}@${domain}`;
29709
29926
  const emailPatch = {
29710
29927
  smtpServer: params.managerHost,
29711
29928
  smtpPort: port,
29712
- userName: params.authUsername ?? recipient,
29929
+ userName: wireUser,
29713
29930
  password: params.authPassword ?? "",
29714
29931
  address1: recipient,
29715
29932
  address2: "",
@@ -30721,16 +30938,16 @@ ${scheduleItems}
30721
30938
  const logger = params.logger ?? this.logger;
30722
30939
  const hlsSegmentDuration = params.hlsSegmentDuration ?? 4;
30723
30940
  const os2 = await import("os");
30724
- const path6 = await import("path");
30725
- const fs6 = await import("fs/promises");
30941
+ const path7 = await import("path");
30942
+ const fs7 = await import("fs/promises");
30726
30943
  const crypto3 = await import("crypto");
30727
- const tempDir = path6.join(
30944
+ const tempDir = path7.join(
30728
30945
  os2.tmpdir(),
30729
30946
  `reolink-hls-${crypto3.randomBytes(8).toString("hex")}`
30730
30947
  );
30731
- await fs6.mkdir(tempDir, { recursive: true });
30732
- const playlistPath = path6.join(tempDir, "playlist.m3u8");
30733
- const segmentPattern = path6.join(tempDir, "segment_%03d.ts");
30948
+ await fs7.mkdir(tempDir, { recursive: true });
30949
+ const playlistPath = path7.join(tempDir, "playlist.m3u8");
30950
+ const segmentPattern = path7.join(tempDir, "segment_%03d.ts");
30734
30951
  const parsed = parseRecordingFileName(params.fileName);
30735
30952
  const durationMs = parsed?.durationMs ?? 3e5;
30736
30953
  const fps = parsed?.framerate && parsed.framerate > 0 ? parsed.framerate : 15;
@@ -30767,13 +30984,13 @@ ${scheduleItems}
30767
30984
  const segments = /* @__PURE__ */ new Map();
30768
30985
  const startSegmentWatcher = () => {
30769
30986
  if (segmentWatcher || !readyResolve) return;
30770
- const firstSegmentPath = path6.join(tempDir, "segment_000.ts");
30987
+ const firstSegmentPath = path7.join(tempDir, "segment_000.ts");
30771
30988
  let checkCount = 0;
30772
30989
  const maxChecks = Math.ceil((hlsSegmentDuration + 2) * 10);
30773
30990
  segmentWatcher = setInterval(async () => {
30774
30991
  checkCount++;
30775
30992
  try {
30776
- const stats = await fs6.stat(firstSegmentPath);
30993
+ const stats = await fs7.stat(firstSegmentPath);
30777
30994
  if (stats.size > 256) {
30778
30995
  if (segmentWatcher) {
30779
30996
  clearInterval(segmentWatcher);
@@ -30915,12 +31132,12 @@ ${scheduleItems}
30915
31132
  ]);
30916
31133
  setTimeout(async () => {
30917
31134
  try {
30918
- const files = await fs6.readdir(tempDir);
31135
+ const files = await fs7.readdir(tempDir);
30919
31136
  for (const file of files) {
30920
- await fs6.unlink(path6.join(tempDir, file)).catch(() => {
31137
+ await fs7.unlink(path7.join(tempDir, file)).catch(() => {
30921
31138
  });
30922
31139
  }
30923
- await fs6.rmdir(tempDir).catch(() => {
31140
+ await fs7.rmdir(tempDir).catch(() => {
30924
31141
  });
30925
31142
  } catch {
30926
31143
  }
@@ -30996,7 +31213,7 @@ ${scheduleItems}
30996
31213
  }
30997
31214
  try {
30998
31215
  const { readFileSync } = require("fs");
30999
- const segmentPath = path6.join(tempDir, name);
31216
+ const segmentPath = path7.join(tempDir, name);
31000
31217
  const data = readFileSync(segmentPath);
31001
31218
  segments.set(name, data);
31002
31219
  return data;
@@ -34169,7 +34386,7 @@ init_BaichuanVideoStream();
34169
34386
  // src/multifocal/compositeStream.ts
34170
34387
  var import_node_child_process7 = require("child_process");
34171
34388
  var import_node_crypto5 = require("crypto");
34172
- var import_node_events5 = require("events");
34389
+ var import_node_events6 = require("events");
34173
34390
  function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pipHeight, margin) {
34174
34391
  const pipW = Math.floor(pipWidth);
34175
34392
  const pipH = Math.floor(pipHeight);
@@ -34197,7 +34414,7 @@ function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pip
34197
34414
  return { x: m, y: m };
34198
34415
  }
34199
34416
  }
34200
- var CompositeStream = class extends import_node_events5.EventEmitter {
34417
+ var CompositeStream = class extends import_node_events6.EventEmitter {
34201
34418
  options;
34202
34419
  widerStream = null;
34203
34420
  teleStream = null;
@@ -36620,7 +36837,7 @@ async function createReplayHttpServer(options) {
36620
36837
  init_BaichuanVideoStream();
36621
36838
 
36622
36839
  // src/baichuan/stream/Go2rtcTcpServer.ts
36623
- var import_node_events6 = require("events");
36840
+ var import_node_events7 = require("events");
36624
36841
  var net4 = __toESM(require("net"), 1);
36625
36842
  init_H264Converter();
36626
36843
  init_H265Converter();
@@ -36732,7 +36949,7 @@ var NativeStreamFanout2 = class {
36732
36949
  this.pumpPromise = null;
36733
36950
  }
36734
36951
  };
36735
- var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEmitter {
36952
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEmitter {
36736
36953
  api;
36737
36954
  channel;
36738
36955
  profile;
@@ -37423,7 +37640,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
37423
37640
  };
37424
37641
 
37425
37642
  // src/baichuan/stream/BaichuanHttpStreamServer.ts
37426
- var import_node_events7 = require("events");
37643
+ var import_node_events8 = require("events");
37427
37644
  var import_node_child_process9 = require("child_process");
37428
37645
  var http4 = __toESM(require("http"), 1);
37429
37646
  var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
@@ -37470,7 +37687,7 @@ function isH264KeyframeFromAnnexB(annexB) {
37470
37687
  }
37471
37688
  return false;
37472
37689
  }
37473
- var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
37690
+ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
37474
37691
  videoStream;
37475
37692
  listenPort;
37476
37693
  path;
@@ -37742,15 +37959,15 @@ var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
37742
37959
  };
37743
37960
 
37744
37961
  // src/baichuan/stream/BaichuanMjpegServer.ts
37745
- var import_node_events9 = require("events");
37962
+ var import_node_events10 = require("events");
37746
37963
  var http5 = __toESM(require("http"), 1);
37747
37964
 
37748
37965
  // src/baichuan/stream/MjpegTransformer.ts
37749
- var import_node_events8 = require("events");
37966
+ var import_node_events9 = require("events");
37750
37967
  var import_node_child_process10 = require("child_process");
37751
37968
  var JPEG_SOI = Buffer.from([255, 216]);
37752
37969
  var JPEG_EOI = Buffer.from([255, 217]);
37753
- var MjpegTransformer = class extends import_node_events8.EventEmitter {
37970
+ var MjpegTransformer = class extends import_node_events9.EventEmitter {
37754
37971
  options;
37755
37972
  ffmpeg = null;
37756
37973
  started = false;
@@ -37949,7 +38166,7 @@ Content-Length: ${frame.length}\r
37949
38166
  // src/baichuan/stream/BaichuanMjpegServer.ts
37950
38167
  init_H264Converter();
37951
38168
  init_H265Converter();
37952
- var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
38169
+ var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
37953
38170
  options;
37954
38171
  clients = /* @__PURE__ */ new Map();
37955
38172
  httpServer = null;
@@ -37971,9 +38188,9 @@ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
37971
38188
  this.started = true;
37972
38189
  const port = this.options.port ?? 8080;
37973
38190
  const host = this.options.host ?? "0.0.0.0";
37974
- const path6 = this.options.path ?? "/mjpeg";
38191
+ const path7 = this.options.path ?? "/mjpeg";
37975
38192
  this.httpServer = http5.createServer((req, res) => {
37976
- this.handleRequest(req, res, path6);
38193
+ this.handleRequest(req, res, path7);
37977
38194
  });
37978
38195
  return new Promise((resolve, reject) => {
37979
38196
  this.httpServer.on("error", (err) => {
@@ -37983,9 +38200,9 @@ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
37983
38200
  this.httpServer.listen(port, host, () => {
37984
38201
  this.log(
37985
38202
  "info",
37986
- `MJPEG server started on http://${host}:${port}${path6}`
38203
+ `MJPEG server started on http://${host}:${port}${path7}`
37987
38204
  );
37988
- this.emit("started", { host, port, path: path6 });
38205
+ this.emit("started", { host, port, path: path7 });
37989
38206
  resolve();
37990
38207
  });
37991
38208
  });
@@ -38230,14 +38447,14 @@ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
38230
38447
  };
38231
38448
 
38232
38449
  // src/baichuan/stream/BaichuanWebRTCServer.ts
38233
- var import_node_events11 = require("events");
38450
+ var import_node_events12 = require("events");
38234
38451
  init_BcMediaAnnexBDecoder();
38235
38452
 
38236
38453
  // src/baichuan/stream/AacToOpusTranscoder.ts
38237
38454
  var import_node_child_process11 = require("child_process");
38238
38455
  var import_node_dgram3 = require("dgram");
38239
- var import_node_events10 = require("events");
38240
- var AacToOpusTranscoder = class extends import_node_events10.EventEmitter {
38456
+ var import_node_events11 = require("events");
38457
+ var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
38241
38458
  opts;
38242
38459
  socket = null;
38243
38460
  ffmpeg = null;
@@ -38454,7 +38671,7 @@ function getH264NalType(nalUnit) {
38454
38671
  function getH265NalType2(nalUnit) {
38455
38672
  return nalUnit[0] >> 1 & 63;
38456
38673
  }
38457
- var BaichuanWebRTCServer = class extends import_node_events11.EventEmitter {
38674
+ var BaichuanWebRTCServer = class extends import_node_events12.EventEmitter {
38458
38675
  options;
38459
38676
  sessions = /* @__PURE__ */ new Map();
38460
38677
  sessionIdCounter = 0;
@@ -39446,7 +39663,7 @@ Error: ${err}`
39446
39663
  };
39447
39664
 
39448
39665
  // src/baichuan/stream/BaichuanHlsServer.ts
39449
- var import_node_events12 = require("events");
39666
+ var import_node_events13 = require("events");
39450
39667
  var import_node_fs = __toESM(require("fs"), 1);
39451
39668
  var import_promises3 = __toESM(require("fs/promises"), 1);
39452
39669
  var import_node_os3 = __toESM(require("os"), 1);
@@ -39526,7 +39743,7 @@ function getNalTypes(codec, annexB) {
39526
39743
  }
39527
39744
  });
39528
39745
  }
39529
- var BaichuanHlsServer = class extends import_node_events12.EventEmitter {
39746
+ var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
39530
39747
  api;
39531
39748
  channel;
39532
39749
  profile;
@@ -40550,10 +40767,10 @@ async function autoDetectDeviceType(inputs) {
40550
40767
  }
40551
40768
 
40552
40769
  // src/multifocal/compositeRtspServer.ts
40553
- var import_node_events13 = require("events");
40770
+ var import_node_events14 = require("events");
40554
40771
  var import_node_child_process13 = require("child_process");
40555
40772
  var net5 = __toESM(require("net"), 1);
40556
- var CompositeRtspServer = class extends import_node_events13.EventEmitter {
40773
+ var CompositeRtspServer = class extends import_node_events14.EventEmitter {
40557
40774
  options;
40558
40775
  compositeStream = null;
40559
40776
  rtspServer = null;
@@ -40846,6 +41063,272 @@ function base64DecodeToBytes(b64) {
40846
41063
  }
40847
41064
  return out.subarray(0, outIdx);
40848
41065
  }
41066
+
41067
+ // src/emailPush/server.ts
41068
+ var import_smtp_server = require("smtp-server");
41069
+ var import_mailparser = require("mailparser");
41070
+
41071
+ // src/emailPush/tls.ts
41072
+ var import_node_fs2 = __toESM(require("fs"), 1);
41073
+ var import_node_path4 = __toESM(require("path"), 1);
41074
+ async function loadEmailPushTls(params) {
41075
+ const certPath = import_node_path4.default.join(params.dir, "cert.pem");
41076
+ const keyPath = import_node_path4.default.join(params.dir, "key.pem");
41077
+ const warn = params.warn ?? ((m) => console.warn(`[email-push-tls] ${m}`));
41078
+ if (!import_node_fs2.default.existsSync(certPath) || !import_node_fs2.default.existsSync(keyPath)) {
41079
+ warn(
41080
+ `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.`
41081
+ );
41082
+ return void 0;
41083
+ }
41084
+ return {
41085
+ cert: import_node_fs2.default.readFileSync(certPath),
41086
+ key: import_node_fs2.default.readFileSync(keyPath)
41087
+ };
41088
+ }
41089
+
41090
+ // src/emailPush/server.ts
41091
+ async function defaultLoadTls(dir, warn) {
41092
+ return loadEmailPushTls({ dir, warn });
41093
+ }
41094
+ function classifyMessage(parsed) {
41095
+ const haystack = `${parsed.subject ?? ""} ${parsed.text ?? ""}`.toLowerCase();
41096
+ if (/person|people|human/.test(haystack)) return "people";
41097
+ if (/vehicle|car|truck/.test(haystack)) return "vehicle";
41098
+ if (/dog[_\s-]?cat|pet|animal/.test(haystack)) return "animal";
41099
+ if (/face/.test(haystack)) return "face";
41100
+ if (/package|parcel/.test(haystack)) return "package";
41101
+ if (/doorbell|ring(?:ing)?\s+button/.test(haystack)) return "doorbell";
41102
+ if (/motion|alarm|alert|detect/.test(haystack)) return "motion";
41103
+ return "other";
41104
+ }
41105
+ function getCameraEmailAddress(cameraId, domain) {
41106
+ return `cam-${cameraId}@${domain}`;
41107
+ }
41108
+ function createEmailPushServer(params) {
41109
+ const log = {
41110
+ debug: params.logger?.debug ?? (() => {
41111
+ }),
41112
+ info: params.logger?.info ?? (() => {
41113
+ }),
41114
+ warn: params.logger?.warn ?? (() => {
41115
+ }),
41116
+ error: params.logger?.error ?? (() => {
41117
+ })
41118
+ };
41119
+ let config = params.config;
41120
+ let server;
41121
+ let status = buildInitialStatus(config);
41122
+ function parseRecipient(rcpt) {
41123
+ const [local, domain] = rcpt.toLowerCase().split("@");
41124
+ if (!local || !domain) return void 0;
41125
+ return { local, domain };
41126
+ }
41127
+ function resolveCameraIdFromRecipient(rcpt) {
41128
+ const parsed = parseRecipient(rcpt);
41129
+ if (!parsed) return void 0;
41130
+ if (parsed.domain !== config.domain.toLowerCase()) return void 0;
41131
+ const match = parsed.local.match(/^cam-(.+)$/);
41132
+ if (!match || !match[1]) return void 0;
41133
+ return params.cameraResolver(match[1]);
41134
+ }
41135
+ async function handleIncomingMessage(cameraId, recipient, raw) {
41136
+ let parsed;
41137
+ try {
41138
+ parsed = await (0, import_mailparser.simpleParser)(raw);
41139
+ } catch (err) {
41140
+ log.warn(
41141
+ `Failed to parse mail for ${cameraId}: ${err instanceof Error ? err.message : err}`
41142
+ );
41143
+ status.messagesRejected++;
41144
+ return;
41145
+ }
41146
+ const receivedAtMs = Date.now();
41147
+ const event = {
41148
+ cameraId,
41149
+ recipient,
41150
+ inferredType: classifyMessage(parsed),
41151
+ receivedAtMs,
41152
+ subject: parsed.subject ?? "",
41153
+ from: typeof parsed.from === "object" && parsed.from !== null && "text" in parsed.from ? String(parsed.from.text) : "",
41154
+ bodyExcerpt: (parsed.text ?? "").slice(0, 500)
41155
+ };
41156
+ status.messagesAccepted++;
41157
+ log.info(
41158
+ `Email push for camera=${cameraId} type=${event.inferredType} subject="${event.subject.slice(0, 80)}"`
41159
+ );
41160
+ emitEmailPushEvent(event);
41161
+ }
41162
+ async function start() {
41163
+ if (server) {
41164
+ log.debug("startEmailPushServer called but server already running");
41165
+ return;
41166
+ }
41167
+ const tlsOptions = config.tls ? await (params.loadTls ?? defaultLoadTls)(
41168
+ config.tlsDir ?? "./email-push-tls",
41169
+ (m) => log.warn(m)
41170
+ ) : void 0;
41171
+ status = buildInitialStatus(config);
41172
+ const next = new import_smtp_server.SMTPServer({
41173
+ authOptional: !config.requireAuth,
41174
+ disabledCommands: config.requireAuth ? [] : ["AUTH"],
41175
+ allowInsecureAuth: !config.tls,
41176
+ size: config.maxMessageBytes,
41177
+ logger: {
41178
+ info: (...args) => log.debug(`[smtp] ${args.map((a) => String(a)).join(" ")}`),
41179
+ debug: (...args) => log.debug(`[smtp] ${args.map((a) => String(a)).join(" ")}`),
41180
+ error: (...args) => log.warn(`[smtp] ${args.map((a) => String(a)).join(" ")}`)
41181
+ },
41182
+ ...tlsOptions ? { secure: false, ...tlsOptions } : {},
41183
+ onConnect(session, callback) {
41184
+ log.info(
41185
+ `SMTP connect from ${session.remoteAddress} (sessionId=${session.id})`
41186
+ );
41187
+ callback();
41188
+ },
41189
+ onClose(session) {
41190
+ log.debug(
41191
+ `SMTP close ${session.remoteAddress} (sessionId=${session.id})`
41192
+ );
41193
+ },
41194
+ onMailFrom(address, session, callback) {
41195
+ log.info(
41196
+ `SMTP MAIL FROM ${address.address} (sessionId=${session.id})`
41197
+ );
41198
+ callback();
41199
+ },
41200
+ onAuth(auth, session, callback) {
41201
+ const expectedUser = config.authUsername;
41202
+ const expectedPass = config.authPassword;
41203
+ if (!expectedUser || !expectedPass) {
41204
+ log.warn(
41205
+ `SMTP AUTH rejected from ${session.remoteAddress} (sessionId=${session.id}): server has no credentials configured`
41206
+ );
41207
+ return callback(new Error("Email push auth not configured"));
41208
+ }
41209
+ const stripDomain = (u) => {
41210
+ const at = u.lastIndexOf("@");
41211
+ if (at < 0) return u;
41212
+ const local = u.slice(0, at);
41213
+ const domain = u.slice(at + 1).toLowerCase();
41214
+ return domain === config.domain.toLowerCase() ? local : u;
41215
+ };
41216
+ const triedUserNorm = stripDomain(auth.username ?? "");
41217
+ const expectedUserNorm = stripDomain(expectedUser);
41218
+ if (triedUserNorm === expectedUserNorm && auth.password === expectedPass) {
41219
+ log.info(
41220
+ `SMTP AUTH ok method=${auth.method} user=${auth.username} (sessionId=${session.id})`
41221
+ );
41222
+ return callback(null, { user: auth.username });
41223
+ }
41224
+ log.warn(
41225
+ `SMTP AUTH FAIL method=${auth.method} from=${session.remoteAddress} triedUser="${auth.username}" expectedUser="${expectedUser}" triedPasswordLen=${auth.password?.length ?? 0} (sessionId=${session.id})`
41226
+ );
41227
+ return callback(new Error("Invalid username or password"));
41228
+ },
41229
+ onRcptTo(address, _session, callback) {
41230
+ const cameraId = resolveCameraIdFromRecipient(address.address);
41231
+ if (!cameraId) {
41232
+ status.messagesRejected++;
41233
+ return callback(
41234
+ new Error(
41235
+ `Unknown recipient ${address.address} (not registered)`
41236
+ )
41237
+ );
41238
+ }
41239
+ callback();
41240
+ },
41241
+ onData(stream, session, callback) {
41242
+ const chunks = [];
41243
+ stream.on("data", (chunk) => chunks.push(chunk));
41244
+ stream.on("end", () => {
41245
+ const recipients = session.envelope.rcptTo?.map((r) => r.address) ?? [];
41246
+ const buffer = Buffer.concat(chunks);
41247
+ const matched = recipients.map((r) => ({
41248
+ recipient: r,
41249
+ cameraId: resolveCameraIdFromRecipient(r)
41250
+ })).filter(
41251
+ (m) => Boolean(m.cameraId)
41252
+ );
41253
+ if (matched.length === 0) {
41254
+ status.messagesRejected++;
41255
+ return callback(new Error("No recognised recipients"));
41256
+ }
41257
+ Promise.all(
41258
+ matched.map(
41259
+ (m) => handleIncomingMessage(m.cameraId, m.recipient, buffer)
41260
+ )
41261
+ ).then(() => callback()).catch((err) => {
41262
+ const msg = err instanceof Error ? err.message : String(err);
41263
+ log.error(`Email push pipeline error: ${msg}`);
41264
+ status.lastErrorMessage = msg;
41265
+ callback(new Error(msg));
41266
+ });
41267
+ });
41268
+ stream.on("error", (err) => {
41269
+ log.warn(`SMTP stream error: ${err.message}`);
41270
+ callback(err);
41271
+ });
41272
+ }
41273
+ });
41274
+ next.on("error", (err) => {
41275
+ status.lastErrorMessage = err.message;
41276
+ log.error(`Email push server error: ${err.message}`);
41277
+ });
41278
+ await new Promise((resolve, reject) => {
41279
+ next.listen(config.port, config.bindHost, () => {
41280
+ status.running = true;
41281
+ status.startedAtMs = Date.now();
41282
+ log.info(
41283
+ `Email push SMTP listening on ${config.bindHost}:${config.port} (domain=${config.domain}, auth=${config.requireAuth}, tls=${config.tls})`
41284
+ );
41285
+ resolve();
41286
+ });
41287
+ next.once("error", reject);
41288
+ });
41289
+ server = next;
41290
+ }
41291
+ async function stop() {
41292
+ if (!server) return;
41293
+ const active = server;
41294
+ server = void 0;
41295
+ await new Promise((resolve) => {
41296
+ active.close(() => resolve());
41297
+ });
41298
+ status.running = false;
41299
+ log.info("Email push SMTP server stopped");
41300
+ }
41301
+ async function restart() {
41302
+ await stop();
41303
+ await start();
41304
+ }
41305
+ return {
41306
+ start,
41307
+ stop,
41308
+ restart,
41309
+ getStatus() {
41310
+ return { ...status };
41311
+ },
41312
+ updateConfig(next) {
41313
+ config = next;
41314
+ }
41315
+ };
41316
+ }
41317
+ function buildInitialStatus(config) {
41318
+ return {
41319
+ enabled: true,
41320
+ running: false,
41321
+ port: config.port,
41322
+ bindHost: config.bindHost,
41323
+ domain: config.domain,
41324
+ requireAuth: config.requireAuth,
41325
+ tls: config.tls,
41326
+ messagesAccepted: 0,
41327
+ messagesRejected: 0,
41328
+ startedAtMs: void 0,
41329
+ lastErrorMessage: void 0
41330
+ };
41331
+ }
40849
41332
  // Annotate the CommonJS export names for ESM import in node:
40850
41333
  0 && (module.exports = {
40851
41334
  AesStreamDecryptor,
@@ -41019,6 +41502,7 @@ function base64DecodeToBytes(b64) {
41019
41502
  ReolinkCgiApi,
41020
41503
  ReolinkHttpClient,
41021
41504
  Rfc4571Muxer,
41505
+ _resetEmailPushBusForTests,
41022
41506
  abilitiesHasAny,
41023
41507
  aesDecrypt,
41024
41508
  aesEncrypt,
@@ -41064,6 +41548,7 @@ function base64DecodeToBytes(b64) {
41064
41548
  createBaichuanEndpointsServer,
41065
41549
  createDebugGateLogger,
41066
41550
  createDiagnosticsBundle,
41551
+ createEmailPushServer,
41067
41552
  createLogger,
41068
41553
  createMjpegBoundary,
41069
41554
  createNativeStream,
@@ -41091,6 +41576,7 @@ function base64DecodeToBytes(b64) {
41091
41576
  discoverViaTcpPortScan,
41092
41577
  discoverViaUdpBroadcast,
41093
41578
  discoverViaUdpDirect,
41579
+ emitEmailPushEvent,
41094
41580
  encodeHeader,
41095
41581
  encodeMotionScopeBitmap,
41096
41582
  encodeMotionSensitivityListXml,
@@ -41107,10 +41593,14 @@ function base64DecodeToBytes(b64) {
41107
41593
  flattenAbilitiesForChannel,
41108
41594
  formatMjpegFrame,
41109
41595
  fullCoverageScope,
41596
+ getCameraEmailAddress,
41110
41597
  getConstructedVideoStreamOptions,
41598
+ getEmailPushCameraResolver,
41111
41599
  getGlobalLogger,
41112
41600
  getH265NalType,
41601
+ getLastEmailPushEvent,
41113
41602
  getMjpegContentType,
41603
+ getRecentEmailPushEvents,
41114
41604
  getSupportItemForChannel,
41115
41605
  getVideoStream,
41116
41606
  getVideoclipClientInfo,
@@ -41125,12 +41615,15 @@ function base64DecodeToBytes(b64) {
41125
41615
  isTcpFailureThatShouldFallbackToUdp,
41126
41616
  isValidH264AnnexBAccessUnit,
41127
41617
  isValidH265AnnexBAccessUnit,
41618
+ loadEmailPushTls,
41619
+ mapEmailPushInferredType,
41128
41620
  maskUid,
41129
41621
  md5HexUpper,
41130
41622
  md5StrModern,
41131
41623
  normalizeDayNightMode,
41132
41624
  normalizeOpenClose,
41133
41625
  normalizeUid,
41626
+ onEmailPushEvent,
41134
41627
  packetizeAacAdtsFrame,
41135
41628
  packetizeAacRawFrame,
41136
41629
  packetizeH264,
@@ -41148,6 +41641,7 @@ function base64DecodeToBytes(b64) {
41148
41641
  runMultifocalDiagnosticsConsecutively,
41149
41642
  sampleStreams,
41150
41643
  sanitizeFixtureData,
41644
+ setEmailPushCameraResolver,
41151
41645
  setGlobalLogger,
41152
41646
  splitAnnexBToNalPayloads,
41153
41647
  splitAnnexBToNals,