mailery 0.19.1 → 0.20.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.
@@ -1,4 +1,4 @@
1
- import { Db, ObjectId, Collection, ClientSession } from 'mongodb';
1
+ import { Db, MongoClientOptions, ObjectId, Collection, ClientSession } from 'mongodb';
2
2
  import { z } from 'zod';
3
3
  import Handlebars from 'handlebars';
4
4
  import IORedis from 'ioredis';
@@ -765,10 +765,31 @@ interface BotFilterConfig {
765
765
  */
766
766
  minOpenDelayMs?: number;
767
767
  }
768
+ /**
769
+ * A MongoDB connection mailery opens itself, on its own driver: `Mailer.init`
770
+ * connects, and `mailer.stop()` closes it. For hosts whose code is on another
771
+ * `mongodb` major (every Mongoose 8 app bundles driver 6), where sharing a
772
+ * `Db` is not possible.
773
+ */
774
+ interface MailerMongoConnection {
775
+ uri: string;
776
+ /** Database name. Defaults to the one named in `uri`. */
777
+ dbName?: string;
778
+ clientOptions?: MongoClientOptions;
779
+ }
780
+ /**
781
+ * Builds the contact adapter on the database mailery connected to. Use it with
782
+ * `mongo`, when the host has no `Db` of its own to hand the adapter:
783
+ * `adapter: (db) => new MongoContactAdapter({ db, collection: 'users' })`.
784
+ */
785
+ type ContactAdapterFactory = (db: Db) => ContactAdapter;
768
786
  interface MailerConfig {
769
- db: Db;
787
+ /** An open database on the host's own client. Give exactly one of `db` or `mongo`. */
788
+ db?: Db;
789
+ /** A connection mailery opens and owns. Give exactly one of `db` or `mongo`. */
790
+ mongo?: MailerMongoConnection;
770
791
  collectionPrefix?: string;
771
- adapter: ContactAdapter;
792
+ adapter: ContactAdapter | ContactAdapterFactory;
772
793
  /**
773
794
  * Optional host-provided template variables. `resolve(contact)` runs at
774
795
  * send/preview/test render time; its result is merged into the template
@@ -980,7 +1001,12 @@ interface MailerConfig {
980
1001
  type ResolvedConfig = Required<Pick<MailerConfig, 'collectionPrefix' | 'requireDoubleOptIn' | 'unsubscribeTokenLifetimeDays' | 'transactionalRespectUnsubscribe' | 'unsubscribeWriteTimeoutMs' | 'doiTemplateSlug' | 'doiTokenLifetimeDays' | 'broadcastConfirmationThreshold' | 'broadcastEnqueueBatchSize' | 'broadcastEnqueueMaxWaiting' | 'workerless' | 'tickIntervalSeconds' | 'sendConcurrency' | 'sendRatePerSecond' | 'softBouncePromotionThreshold' | 'softBouncePromotionWindowDays' | 'webhookRetryAttempts' | 'sendRetryAttempts' | 'trackOpens' | 'trackClicks' | 'storeTrackingIp' | 'storeRenderedBody' | 'requireSignedTrackingUrls' | 'trackingUrlLifetimeDays'>> & {
981
1002
  circuitBreaker: CircuitBreakerThresholds;
982
1003
  broadcastStopRules: BroadcastStopRules;
983
- } & MailerConfig;
1004
+ } & StorageResolvedConfig;
1005
+ /** `MailerConfig` once `Mailer.init` has opened storage: one `db`, one built adapter. */
1006
+ type StorageResolvedConfig = Omit<MailerConfig, 'db' | 'mongo' | 'adapter'> & {
1007
+ db: Db;
1008
+ adapter: ContactAdapter;
1009
+ };
984
1010
 
985
1011
  /**
986
1012
  * Mongo collection helpers + indexes for every mailer-owned collection.
@@ -1717,6 +1743,8 @@ declare class Mailer {
1717
1743
  readonly config: ResolvedConfig;
1718
1744
  readonly events: EventRegistry;
1719
1745
  private queueDriver;
1746
+ /** The client `Mailer.init` opened from `config.mongo`; closed by `stop()`. Null when the host gave `db`. */
1747
+ private ownedClient;
1720
1748
  private workersStarted;
1721
1749
  private runnerContext;
1722
1750
  private constructor();
@@ -1869,4 +1897,4 @@ declare class NullProvider implements MailProvider {
1869
1897
  reset(): void;
1870
1898
  }
1871
1899
 
1872
- export { type VarsResolveInfo as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SendDoc as G, type HealthDoc as H, type SendStatus as I, type SenderDomainConfig as J, type SenderDomainRegistry as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainValidation as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SubscriptionDoc as U, type SubscriptionStatus as V, type SuppressionDoc as W, type SuppressionReason as X, type TemplateKind as Y, type TemplateVersionDoc as Z, type VarsAdapter as _, type Contact as a, type WebhookEventDoc as a0, defineVars as a1, ensureIndexes as a2, getCollections as a3, validateSenderDomain as a4, varsJsonSchema as a5, type FlowTrigger as a6, type QueueDriverConfig as a7, type AdapterSort as b, type SendResult as c, type MailTesterFeedback as d, Mailer as e, type Collections as f, type FlowDoc as g, type SuppressionScope as h, type SegmentFilter as i, type AuditLogDoc as j, type BroadcastDoc as k, type BroadcastOrder as l, type BroadcastPauseReason as m, type BroadcastStatus as n, type CircuitBreakerThresholds as o, type ContactTagDoc as p, type FlowGoal as q, type FlowRunDoc as r, type FlowRunStatus as s, type FlowVersionDoc as t, type HealthStatus as u, type MailerConfig as v, NullProvider as w, RESERVED_VAR_KEYS as x, type RedisOptions as y, type SegmentDefinition as z };
1900
+ export { type TemplateVersionDoc as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type RedisOptions as G, type HealthDoc as H, type SegmentDefinition as I, type SendDoc as J, type SendStatus as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainConfig as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SenderDomainRegistry as U, type SenderDomainValidation as V, type SubscriptionDoc as W, type SubscriptionStatus as X, type SuppressionDoc as Y, type SuppressionReason as Z, type TemplateKind as _, type Contact as a, type VarsAdapter as a0, type VarsResolveInfo as a1, type WebhookEventDoc as a2, defineVars as a3, ensureIndexes as a4, getCollections as a5, validateSenderDomain as a6, varsJsonSchema as a7, type FlowTrigger as a8, type QueueDriverConfig as a9, type AdapterSort as b, type SendResult as c, type MailTesterFeedback as d, Mailer as e, type Collections as f, type FlowDoc as g, type SuppressionScope as h, type SegmentFilter as i, type AuditLogDoc as j, type BroadcastDoc as k, type BroadcastOrder as l, type BroadcastPauseReason as m, type BroadcastStatus as n, type CircuitBreakerThresholds as o, type ContactAdapterFactory as p, type ContactTagDoc as q, type FlowGoal as r, type FlowRunDoc as s, type FlowRunStatus as t, type FlowVersionDoc as u, type HealthStatus as v, type MailerConfig as w, type MailerMongoConnection as x, NullProvider as y, RESERVED_VAR_KEYS as z };
@@ -1,4 +1,4 @@
1
- import { Db, ObjectId, Collection, ClientSession } from 'mongodb';
1
+ import { Db, MongoClientOptions, ObjectId, Collection, ClientSession } from 'mongodb';
2
2
  import { z } from 'zod';
3
3
  import Handlebars from 'handlebars';
4
4
  import IORedis from 'ioredis';
@@ -765,10 +765,31 @@ interface BotFilterConfig {
765
765
  */
766
766
  minOpenDelayMs?: number;
767
767
  }
768
+ /**
769
+ * A MongoDB connection mailery opens itself, on its own driver: `Mailer.init`
770
+ * connects, and `mailer.stop()` closes it. For hosts whose code is on another
771
+ * `mongodb` major (every Mongoose 8 app bundles driver 6), where sharing a
772
+ * `Db` is not possible.
773
+ */
774
+ interface MailerMongoConnection {
775
+ uri: string;
776
+ /** Database name. Defaults to the one named in `uri`. */
777
+ dbName?: string;
778
+ clientOptions?: MongoClientOptions;
779
+ }
780
+ /**
781
+ * Builds the contact adapter on the database mailery connected to. Use it with
782
+ * `mongo`, when the host has no `Db` of its own to hand the adapter:
783
+ * `adapter: (db) => new MongoContactAdapter({ db, collection: 'users' })`.
784
+ */
785
+ type ContactAdapterFactory = (db: Db) => ContactAdapter;
768
786
  interface MailerConfig {
769
- db: Db;
787
+ /** An open database on the host's own client. Give exactly one of `db` or `mongo`. */
788
+ db?: Db;
789
+ /** A connection mailery opens and owns. Give exactly one of `db` or `mongo`. */
790
+ mongo?: MailerMongoConnection;
770
791
  collectionPrefix?: string;
771
- adapter: ContactAdapter;
792
+ adapter: ContactAdapter | ContactAdapterFactory;
772
793
  /**
773
794
  * Optional host-provided template variables. `resolve(contact)` runs at
774
795
  * send/preview/test render time; its result is merged into the template
@@ -980,7 +1001,12 @@ interface MailerConfig {
980
1001
  type ResolvedConfig = Required<Pick<MailerConfig, 'collectionPrefix' | 'requireDoubleOptIn' | 'unsubscribeTokenLifetimeDays' | 'transactionalRespectUnsubscribe' | 'unsubscribeWriteTimeoutMs' | 'doiTemplateSlug' | 'doiTokenLifetimeDays' | 'broadcastConfirmationThreshold' | 'broadcastEnqueueBatchSize' | 'broadcastEnqueueMaxWaiting' | 'workerless' | 'tickIntervalSeconds' | 'sendConcurrency' | 'sendRatePerSecond' | 'softBouncePromotionThreshold' | 'softBouncePromotionWindowDays' | 'webhookRetryAttempts' | 'sendRetryAttempts' | 'trackOpens' | 'trackClicks' | 'storeTrackingIp' | 'storeRenderedBody' | 'requireSignedTrackingUrls' | 'trackingUrlLifetimeDays'>> & {
981
1002
  circuitBreaker: CircuitBreakerThresholds;
982
1003
  broadcastStopRules: BroadcastStopRules;
983
- } & MailerConfig;
1004
+ } & StorageResolvedConfig;
1005
+ /** `MailerConfig` once `Mailer.init` has opened storage: one `db`, one built adapter. */
1006
+ type StorageResolvedConfig = Omit<MailerConfig, 'db' | 'mongo' | 'adapter'> & {
1007
+ db: Db;
1008
+ adapter: ContactAdapter;
1009
+ };
984
1010
 
985
1011
  /**
986
1012
  * Mongo collection helpers + indexes for every mailer-owned collection.
@@ -1717,6 +1743,8 @@ declare class Mailer {
1717
1743
  readonly config: ResolvedConfig;
1718
1744
  readonly events: EventRegistry;
1719
1745
  private queueDriver;
1746
+ /** The client `Mailer.init` opened from `config.mongo`; closed by `stop()`. Null when the host gave `db`. */
1747
+ private ownedClient;
1720
1748
  private workersStarted;
1721
1749
  private runnerContext;
1722
1750
  private constructor();
@@ -1869,4 +1897,4 @@ declare class NullProvider implements MailProvider {
1869
1897
  reset(): void;
1870
1898
  }
1871
1899
 
1872
- export { type VarsResolveInfo as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SendDoc as G, type HealthDoc as H, type SendStatus as I, type SenderDomainConfig as J, type SenderDomainRegistry as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainValidation as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SubscriptionDoc as U, type SubscriptionStatus as V, type SuppressionDoc as W, type SuppressionReason as X, type TemplateKind as Y, type TemplateVersionDoc as Z, type VarsAdapter as _, type Contact as a, type WebhookEventDoc as a0, defineVars as a1, ensureIndexes as a2, getCollections as a3, validateSenderDomain as a4, varsJsonSchema as a5, type FlowTrigger as a6, type QueueDriverConfig as a7, type AdapterSort as b, type SendResult as c, type MailTesterFeedback as d, Mailer as e, type Collections as f, type FlowDoc as g, type SuppressionScope as h, type SegmentFilter as i, type AuditLogDoc as j, type BroadcastDoc as k, type BroadcastOrder as l, type BroadcastPauseReason as m, type BroadcastStatus as n, type CircuitBreakerThresholds as o, type ContactTagDoc as p, type FlowGoal as q, type FlowRunDoc as r, type FlowRunStatus as s, type FlowVersionDoc as t, type HealthStatus as u, type MailerConfig as v, NullProvider as w, RESERVED_VAR_KEYS as x, type RedisOptions as y, type SegmentDefinition as z };
1900
+ export { type TemplateVersionDoc as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type RedisOptions as G, type HealthDoc as H, type SegmentDefinition as I, type SendDoc as J, type SendStatus as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainConfig as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SenderDomainRegistry as U, type SenderDomainValidation as V, type SubscriptionDoc as W, type SubscriptionStatus as X, type SuppressionDoc as Y, type SuppressionReason as Z, type TemplateKind as _, type Contact as a, type VarsAdapter as a0, type VarsResolveInfo as a1, type WebhookEventDoc as a2, defineVars as a3, ensureIndexes as a4, getCollections as a5, validateSenderDomain as a6, varsJsonSchema as a7, type FlowTrigger as a8, type QueueDriverConfig as a9, type AdapterSort as b, type SendResult as c, type MailTesterFeedback as d, Mailer as e, type Collections as f, type FlowDoc as g, type SuppressionScope as h, type SegmentFilter as i, type AuditLogDoc as j, type BroadcastDoc as k, type BroadcastOrder as l, type BroadcastPauseReason as m, type BroadcastStatus as n, type CircuitBreakerThresholds as o, type ContactAdapterFactory as p, type ContactTagDoc as q, type FlowGoal as r, type FlowRunDoc as s, type FlowRunStatus as t, type FlowVersionDoc as u, type HealthStatus as v, type MailerConfig as w, type MailerMongoConnection as x, NullProvider as y, RESERVED_VAR_KEYS as z };
package/dist/testing.cjs CHANGED
@@ -16140,7 +16140,7 @@ function derivePlaintext(html) {
16140
16140
  var PLAIN_TEXT = { noEscape: true };
16141
16141
  async function renderTemplate(template, ctx, opts = {}) {
16142
16142
  const hb = makeHandlebars(opts.helpers);
16143
- const subject = hb.compile(template.subject)(ctx);
16143
+ const subject = hb.compile(template.subject, PLAIN_TEXT)(ctx).replace(/[\r\n]+/g, " ");
16144
16144
  const preheader = hb.compile(template.preheader)(ctx);
16145
16145
  let html;
16146
16146
  if (template.body.html) {
@@ -18479,6 +18479,7 @@ async function processScheduledBroadcasts2(ctx) {
18479
18479
  }
18480
18480
 
18481
18481
  // src/server/mailer.ts
18482
+ var STORAGE_REQUIRED = "Mailer.init: give exactly one of `db` (an open Db) or `mongo` (a connection mailery opens)";
18482
18483
  var Mailer = class _Mailer {
18483
18484
  db;
18484
18485
  collections;
@@ -18488,6 +18489,8 @@ var Mailer = class _Mailer {
18488
18489
  config;
18489
18490
  events;
18490
18491
  queueDriver;
18492
+ /** The client `Mailer.init` opened from `config.mongo`; closed by `stop()`. Null when the host gave `db`. */
18493
+ ownedClient;
18491
18494
  workersStarted = false;
18492
18495
  runnerContext;
18493
18496
  constructor(args) {
@@ -18497,6 +18500,7 @@ var Mailer = class _Mailer {
18497
18500
  this.adapter = args.adapter;
18498
18501
  this.providers = args.providers;
18499
18502
  this.queueDriver = args.queueDriver;
18503
+ this.ownedClient = args.ownedClient;
18500
18504
  this.queues = args.queueDriver.queues;
18501
18505
  this.events = args.events;
18502
18506
  this.runnerContext = {
@@ -18593,36 +18597,58 @@ var Mailer = class _Mailer {
18593
18597
  });
18594
18598
  }
18595
18599
  static async init(input) {
18596
- const config = resolveConfig(input);
18597
- if (!resolveProvider(config.providers, config.defaultProvider)) {
18600
+ if (input.db == null === (input.mongo == null)) throw new Error(STORAGE_REQUIRED);
18601
+ if (!resolveProvider(input.providers, input.defaultProvider)) {
18598
18602
  throw new Error(
18599
- `defaultProvider "${config.defaultProvider}" is not a registered provider. Registered: ${registeredProviderNames(config.providers).join(", ") || "(none)"}`
18603
+ `defaultProvider "${input.defaultProvider}" is not a registered provider. Registered: ${registeredProviderNames(input.providers).join(", ") || "(none)"}`
18600
18604
  );
18601
18605
  }
18602
- if (config.defaultTransactionalProvider != null && !resolveProvider(config.providers, config.defaultTransactionalProvider)) {
18606
+ if (input.defaultTransactionalProvider != null && !resolveProvider(input.providers, input.defaultTransactionalProvider)) {
18603
18607
  throw new Error(
18604
- `defaultTransactionalProvider "${config.defaultTransactionalProvider}" is not a registered provider. Registered: ${registeredProviderNames(config.providers).join(", ") || "(none)"}`
18608
+ `defaultTransactionalProvider "${input.defaultTransactionalProvider}" is not a registered provider. Registered: ${registeredProviderNames(input.providers).join(", ") || "(none)"}`
18605
18609
  );
18606
18610
  }
18607
- if (config.varsAdapter) {
18611
+ if (input.varsAdapter) {
18608
18612
  const { assertNoReservedVarKeys: assertNoReservedVarKeys2 } = await Promise.resolve().then(() => (init_vars(), vars_exports));
18609
- assertNoReservedVarKeys2(config.varsAdapter);
18610
- }
18611
- const collections = getCollections(config.db, config.collectionPrefix);
18612
- await ensureIndexes(config.db, config.collectionPrefix);
18613
- const queueDriver = await createQueueDriver(config.queue, config.db);
18614
- if (!config.workerless && config.queue.driver !== "noop") {
18615
- await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
18616
- }
18617
- return new _Mailer({
18618
- config,
18619
- db: config.db,
18620
- collections,
18621
- adapter: config.adapter,
18622
- providers: config.providers,
18623
- queueDriver,
18624
- events: new EventRegistry()
18625
- });
18613
+ assertNoReservedVarKeys2(input.varsAdapter);
18614
+ }
18615
+ let ownedClient = null;
18616
+ let db;
18617
+ if (input.mongo) {
18618
+ const { MongoClient: MongoClient2 } = await import('mongodb');
18619
+ ownedClient = await MongoClient2.connect(input.mongo.uri, input.mongo.clientOptions);
18620
+ db = ownedClient.db(input.mongo.dbName);
18621
+ } else if (input.db) {
18622
+ db = input.db;
18623
+ } else {
18624
+ throw new Error(STORAGE_REQUIRED);
18625
+ }
18626
+ try {
18627
+ const config = resolveConfig({
18628
+ ...input,
18629
+ db,
18630
+ adapter: typeof input.adapter === "function" ? input.adapter(db) : input.adapter
18631
+ });
18632
+ const collections = getCollections(config.db, config.collectionPrefix);
18633
+ await ensureIndexes(config.db, config.collectionPrefix);
18634
+ const queueDriver = await createQueueDriver(config.queue, config.db);
18635
+ if (!config.workerless && config.queue.driver !== "noop") {
18636
+ await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
18637
+ }
18638
+ return new _Mailer({
18639
+ config,
18640
+ db: config.db,
18641
+ collections,
18642
+ adapter: config.adapter,
18643
+ providers: config.providers,
18644
+ queueDriver,
18645
+ events: new EventRegistry(),
18646
+ ownedClient
18647
+ });
18648
+ } catch (err) {
18649
+ await ownedClient?.close();
18650
+ throw err;
18651
+ }
18626
18652
  }
18627
18653
  // -------------------------------------------------------------------------
18628
18654
  // Event registration + firing
@@ -19095,6 +19121,8 @@ var Mailer = class _Mailer {
19095
19121
  async stop() {
19096
19122
  await this.queueDriver.close();
19097
19123
  this.workersStarted = false;
19124
+ await this.ownedClient?.close();
19125
+ this.ownedClient = null;
19098
19126
  }
19099
19127
  /** Used internally by the admin router and tests; not part of the public API. */
19100
19128
  getRunnerContext() {