@apocaliss92/nodelink-js 0.4.28 → 0.4.30

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.d.cts CHANGED
@@ -5623,6 +5623,19 @@ interface EmailPushEvent {
5623
5623
  from: string;
5624
5624
  /** Raw body text excerpt (max 500 chars). */
5625
5625
  bodyExcerpt: string;
5626
+ /**
5627
+ * First image attachment from the e-mail, when present. The camera
5628
+ * sends a JPEG snapshot of the trigger frame on motion when
5629
+ * `attachmentType=picture`; consumers can republish it to MQTT image
5630
+ * entities or cache it as the camera's `lastPicture` so battery
5631
+ * cameras don't have to be woken up to get a fresh thumbnail.
5632
+ * Kept in memory only — the bus does not persist it.
5633
+ */
5634
+ attachment?: {
5635
+ contentType: string;
5636
+ data: Buffer;
5637
+ filename?: string;
5638
+ };
5626
5639
  }
5627
5640
  type EventHandler = (event: EmailPushEvent) => void;
5628
5641
  type CameraResolver = (recipient: string) => string | undefined;
@@ -6310,9 +6323,19 @@ declare class ReolinkBaichuanApi {
6310
6323
  subscribeEmailPushEvents(params: {
6311
6324
  cameraId: string;
6312
6325
  channel?: number;
6326
+ /**
6327
+ * Optional handler invoked with the *full* `EmailPushEvent`
6328
+ * after the simple-event dispatch. Useful for consumers that
6329
+ * need fields beyond what `onSimpleEvent` carries — most
6330
+ * notably `event.attachment` (the JPEG snapshot from the
6331
+ * trigger frame) which a downstream plugin can republish to
6332
+ * MQTT or cache as the camera's last picture.
6333
+ */
6334
+ onEvent?: (event: EmailPushEvent) => void;
6313
6335
  } | {
6314
6336
  match: (event: EmailPushEvent) => boolean;
6315
6337
  channel?: number;
6338
+ onEvent?: (event: EmailPushEvent) => void;
6316
6339
  }): () => void;
6317
6340
  /**
6318
6341
  * Subscribe to minimal high-level events.
package/dist/index.d.ts CHANGED
@@ -4807,6 +4807,19 @@ export declare interface EmailPushEvent {
4807
4807
  from: string;
4808
4808
  /** Raw body text excerpt (max 500 chars). */
4809
4809
  bodyExcerpt: string;
4810
+ /**
4811
+ * First image attachment from the e-mail, when present. The camera
4812
+ * sends a JPEG snapshot of the trigger frame on motion when
4813
+ * `attachmentType=picture`; consumers can republish it to MQTT image
4814
+ * entities or cache it as the camera's `lastPicture` so battery
4815
+ * cameras don't have to be woken up to get a fresh thumbnail.
4816
+ * Kept in memory only — the bus does not persist it.
4817
+ */
4818
+ attachment?: {
4819
+ contentType: string;
4820
+ data: Buffer;
4821
+ filename?: string;
4822
+ };
4810
4823
  }
4811
4824
 
4812
4825
  /**
@@ -7295,9 +7308,19 @@ export declare class ReolinkBaichuanApi {
7295
7308
  subscribeEmailPushEvents(params: {
7296
7309
  cameraId: string;
7297
7310
  channel?: number;
7311
+ /**
7312
+ * Optional handler invoked with the *full* `EmailPushEvent`
7313
+ * after the simple-event dispatch. Useful for consumers that
7314
+ * need fields beyond what `onSimpleEvent` carries — most
7315
+ * notably `event.attachment` (the JPEG snapshot from the
7316
+ * trigger frame) which a downstream plugin can republish to
7317
+ * MQTT or cache as the camera's last picture.
7318
+ */
7319
+ onEvent?: (event: EmailPushEvent) => void;
7298
7320
  } | {
7299
7321
  match: (event: EmailPushEvent) => boolean;
7300
7322
  channel?: number;
7323
+ onEvent?: (event: EmailPushEvent) => void;
7301
7324
  }): () => void;
7302
7325
  /**
7303
7326
  * Subscribe to minimal high-level events.
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ import {
65
65
  setEmailPushCameraResolver,
66
66
  setGlobalLogger,
67
67
  xmlIndicatesFloodlight
68
- } from "./chunk-NQ7D5TLR.js";
68
+ } from "./chunk-AZZKLRJV.js";
69
69
  import {
70
70
  ReolinkCgiApi,
71
71
  ReolinkHttpClient,
@@ -8284,6 +8284,7 @@ function base64DecodeToBytes(b64) {
8284
8284
  }
8285
8285
 
8286
8286
  // src/emailPush/server.ts
8287
+ import { format as utilFormat } from "util";
8287
8288
  import { SMTPServer } from "smtp-server";
8288
8289
  import { simpleParser } from "mailparser";
8289
8290
 
@@ -8363,6 +8364,9 @@ function createEmailPushServer(params) {
8363
8364
  return;
8364
8365
  }
8365
8366
  const receivedAtMs = Date.now();
8367
+ const imageAttachment = (parsed.attachments ?? []).find(
8368
+ (a) => a && typeof a.contentType === "string" && a.contentType.toLowerCase().startsWith("image/") && Buffer.isBuffer(a.content)
8369
+ );
8366
8370
  const event = {
8367
8371
  cameraId,
8368
8372
  recipient,
@@ -8370,11 +8374,18 @@ function createEmailPushServer(params) {
8370
8374
  receivedAtMs,
8371
8375
  subject: parsed.subject ?? "",
8372
8376
  from: typeof parsed.from === "object" && parsed.from !== null && "text" in parsed.from ? String(parsed.from.text) : "",
8373
- bodyExcerpt: (parsed.text ?? "").slice(0, 500)
8377
+ bodyExcerpt: (parsed.text ?? "").slice(0, 500),
8378
+ ...imageAttachment ? {
8379
+ attachment: {
8380
+ contentType: imageAttachment.contentType,
8381
+ data: imageAttachment.content,
8382
+ ...imageAttachment.filename ? { filename: imageAttachment.filename } : {}
8383
+ }
8384
+ } : {}
8374
8385
  };
8375
8386
  status.messagesAccepted++;
8376
8387
  log.info(
8377
- `Email push for camera=${cameraId} type=${event.inferredType} subject="${event.subject.slice(0, 80)}"`
8388
+ `Email push for camera=${cameraId} type=${event.inferredType} attachment=${event.attachment ? `${event.attachment.contentType} ${event.attachment.data.length}B` : "none"} subject="${event.subject.slice(0, 80)}"`
8378
8389
  );
8379
8390
  emitEmailPushEvent(event);
8380
8391
  }
@@ -8393,10 +8404,19 @@ function createEmailPushServer(params) {
8393
8404
  disabledCommands: config.requireAuth ? [] : ["AUTH"],
8394
8405
  allowInsecureAuth: !config.tls,
8395
8406
  size: config.maxMessageBytes,
8407
+ // smtp-server invokes its internal logger as
8408
+ // logger.info(metadata, formatString, ...args)
8409
+ // where `metadata` is an opaque session/connection object and
8410
+ // `formatString` may contain printf-style placeholders.
8411
+ // Naive `.map(String).join(" ")` would surface `[object Object]`
8412
+ // and leave `%s` unresolved (which is what the user was seeing).
8413
+ // Here we strip the metadata (keeping a compact `tnx`/`cid` tag
8414
+ // when present), then apply `util.format` so placeholders are
8415
+ // expanded by Node exactly like the smtp-server defaults do.
8396
8416
  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(" ")}`)
8417
+ info: (...args) => log.info(formatSmtpLogArgs(args)),
8418
+ debug: (...args) => log.debug(formatSmtpLogArgs(args)),
8419
+ error: (...args) => log.warn(formatSmtpLogArgs(args))
8400
8420
  },
8401
8421
  ...tlsOptions ? { secure: false, ...tlsOptions } : {},
8402
8422
  onConnect(session, callback) {
@@ -8445,24 +8465,37 @@ function createEmailPushServer(params) {
8445
8465
  );
8446
8466
  return callback(new Error("Invalid username or password"));
8447
8467
  },
8448
- onRcptTo(address, _session, callback) {
8468
+ onRcptTo(address, session, callback) {
8449
8469
  const cameraId = resolveCameraIdFromRecipient(address.address);
8450
8470
  if (!cameraId) {
8451
8471
  status.messagesRejected++;
8472
+ log.warn(
8473
+ `SMTP RCPT TO rejected ${address.address} \u2014 unknown recipient (sessionId=${session.id})`
8474
+ );
8452
8475
  return callback(
8453
8476
  new Error(
8454
8477
  `Unknown recipient ${address.address} (not registered)`
8455
8478
  )
8456
8479
  );
8457
8480
  }
8481
+ log.info(
8482
+ `SMTP RCPT TO ${address.address} \u2192 camera=${cameraId} (sessionId=${session.id})`
8483
+ );
8458
8484
  callback();
8459
8485
  },
8460
8486
  onData(stream, session, callback) {
8487
+ const startedAt = Date.now();
8488
+ log.debug(
8489
+ `SMTP DATA start (sessionId=${session.id} from=${session.envelope.mailFrom ? session.envelope.mailFrom.address : "?"})`
8490
+ );
8461
8491
  const chunks = [];
8462
8492
  stream.on("data", (chunk) => chunks.push(chunk));
8463
8493
  stream.on("end", () => {
8464
8494
  const recipients = session.envelope.rcptTo?.map((r) => r.address) ?? [];
8465
8495
  const buffer = Buffer.concat(chunks);
8496
+ log.info(
8497
+ `SMTP DATA received ${buffer.length}B in ${Date.now() - startedAt}ms for ${recipients.length} recipient(s) (sessionId=${session.id})`
8498
+ );
8466
8499
  const matched = recipients.map((r) => ({
8467
8500
  recipient: r,
8468
8501
  cameraId: resolveCameraIdFromRecipient(r)
@@ -8471,6 +8504,9 @@ function createEmailPushServer(params) {
8471
8504
  );
8472
8505
  if (matched.length === 0) {
8473
8506
  status.messagesRejected++;
8507
+ log.warn(
8508
+ `SMTP DATA dropped \u2014 no recognised recipients in [${recipients.join(", ")}] (sessionId=${session.id})`
8509
+ );
8474
8510
  return callback(new Error("No recognised recipients"));
8475
8511
  }
8476
8512
  Promise.all(
@@ -8479,13 +8515,15 @@ function createEmailPushServer(params) {
8479
8515
  )
8480
8516
  ).then(() => callback()).catch((err) => {
8481
8517
  const msg = err instanceof Error ? err.message : String(err);
8482
- log.error(`Email push pipeline error: ${msg}`);
8518
+ log.error(
8519
+ `Email push pipeline error (sessionId=${session.id}): ${msg}`
8520
+ );
8483
8521
  status.lastErrorMessage = msg;
8484
8522
  callback(new Error(msg));
8485
8523
  });
8486
8524
  });
8487
8525
  stream.on("error", (err) => {
8488
- log.warn(`SMTP stream error: ${err.message}`);
8526
+ log.warn(`SMTP stream error (sessionId=${session.id}): ${err.message}`);
8489
8527
  callback(err);
8490
8528
  });
8491
8529
  }
@@ -8533,6 +8571,25 @@ function createEmailPushServer(params) {
8533
8571
  }
8534
8572
  };
8535
8573
  }
8574
+ function formatSmtpLogArgs(args) {
8575
+ if (args.length === 0) return "[smtp]";
8576
+ const [first, ...rest] = args;
8577
+ let tag = "";
8578
+ let formatArgs = args;
8579
+ if (first && typeof first === "object" && !Array.isArray(first)) {
8580
+ const meta = first;
8581
+ const tnx = typeof meta.tnx === "string" ? meta.tnx : void 0;
8582
+ const cid = typeof meta.cid === "string" ? meta.cid : void 0;
8583
+ const sid = typeof meta.sid === "string" ? meta.sid : void 0;
8584
+ const bits = [];
8585
+ if (tnx) bits.push(tnx);
8586
+ if (cid) bits.push(`cid=${cid}`);
8587
+ if (sid) bits.push(`sid=${sid}`);
8588
+ if (bits.length > 0) tag = ` [${bits.join(" ")}]`;
8589
+ formatArgs = rest;
8590
+ }
8591
+ return `[smtp]${tag} ${utilFormat(...formatArgs)}`;
8592
+ }
8536
8593
  function buildInitialStatus(config) {
8537
8594
  return {
8538
8595
  enabled: true,