mailery 0.3.2 → 0.5.1

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.
@@ -430,6 +430,94 @@ interface RedisOptions {
430
430
  username?: string;
431
431
  tls?: boolean;
432
432
  }
433
+ interface DnsblListSpec {
434
+ /** DNS suffix to query, e.g. `'dbl.spamhaus.org'`. */
435
+ host: string;
436
+ /** Display label for the UI, e.g. `'Spamhaus DBL'`. */
437
+ label: string;
438
+ }
439
+ interface DnsblConfig {
440
+ /** Domain block lists (DBLs) — queried as `<domain>.<host>`. */
441
+ domainLists?: DnsblListSpec[];
442
+ /** IP block lists — queried as `<reversed-octets>.<host>`. */
443
+ ipLists?: DnsblListSpec[];
444
+ /** Dedicated sending IPs to check. Most operators on shared ESPs leave empty. */
445
+ dedicatedIps?: string[];
446
+ /** Hours between automatic runs. Default 24. Set to 0 to disable scheduled runs. */
447
+ intervalHours?: number;
448
+ }
449
+ interface MailTesterConfig {
450
+ /** API key from your Mail-Tester paid plan. */
451
+ apiKey: string;
452
+ /** Minimum score (0-10) below which publish is blocked. Default 8.0. */
453
+ minScore?: number;
454
+ /**
455
+ * How long a successful score remains cached for the same content. Default 24.
456
+ * Re-publishing the same body within the window does not burn a credit.
457
+ */
458
+ cacheHours?: number;
459
+ /** API base URL. Defaults to `https://mail-tester.com/api`. Override for staging or alternate provider. */
460
+ baseUrl?: string;
461
+ }
462
+ interface DmarcSourceTag {
463
+ /** Source IP, e.g. `'149.72.45.10'`. */
464
+ ip: string;
465
+ /** Operator-provided label, e.g. `'SendGrid (transactional)'` or `'Hubspot'`. */
466
+ label: string;
467
+ /** When true, ignore failures from this source in the alignment-trend headline. */
468
+ ignored?: boolean;
469
+ }
470
+ interface DmarcConfig {
471
+ /**
472
+ * Operator-tagged sources. After RUA reports start arriving, the operator
473
+ * marks each source IP as known/legit (with a label) or rogue. Tagged
474
+ * sources are excluded from the headline failure count, surfacing only
475
+ * unknown senders.
476
+ */
477
+ knownSources?: DmarcSourceTag[];
478
+ /**
479
+ * How many days of failure rows to retain. Default 90. Older rows are
480
+ * trimmed by the daily tick to keep the collection bounded.
481
+ */
482
+ retentionDays?: number;
483
+ }
484
+ interface SndsConfig {
485
+ /**
486
+ * SNDS automated-access key. Obtained per-account from the SNDS portal at
487
+ * https://sendersupport.olc.protection.outlook.com/snds/ once each
488
+ * sending IP has been added and verified.
489
+ */
490
+ accessKey: string;
491
+ /**
492
+ * Optional IP filter — when set, only rows for these IPs are persisted.
493
+ * Useful when an SNDS account covers a wider IP pool than this mailery
494
+ * deployment uses. Defaults to keeping all rows the API returns.
495
+ */
496
+ ips?: string[];
497
+ /** Hours between automatic pulls. Default 24. Set to 0 to disable scheduled pulls. */
498
+ intervalHours?: number;
499
+ }
500
+ interface PostmasterConfig {
501
+ /** OAuth client ID from a Google Cloud project with Postmaster Tools API enabled. */
502
+ clientId: string;
503
+ clientSecret: string;
504
+ /**
505
+ * A refresh token obtained via the OAuth consent flow with scope
506
+ * `https://www.googleapis.com/auth/postmaster.readonly` for the user that
507
+ * verified the domain(s) in Postmaster Tools. See
508
+ * docs/guide/deliverability.md → "Google Postmaster Tools" for the
509
+ * one-time setup procedure.
510
+ */
511
+ refreshToken: string;
512
+ /**
513
+ * Domains to pull data for. When omitted, mailery uses every domain from the
514
+ * senderDomains registry plus the From defaults. Each must be verified in
515
+ * Postmaster Tools under the same Google account.
516
+ */
517
+ domains?: string[];
518
+ /** Hours between automatic pulls. Default 24. Set to 0 to disable scheduled pulls. */
519
+ intervalHours?: number;
520
+ }
433
521
  interface CircuitBreakerThresholds {
434
522
  /** Trip when last-hour hard-bounce rate >= this percent (e.g. 2 = 2%). */
435
523
  hardBounceRatePctTrip: number;
@@ -481,6 +569,41 @@ interface MailerConfig {
481
569
  /** How long the DOI token stays valid. Default 7 days. */
482
570
  doiTokenLifetimeDays?: number;
483
571
  circuitBreaker?: Partial<CircuitBreakerThresholds>;
572
+ /**
573
+ * Daily DNS-based block-list checks on sender domains and (optionally) any
574
+ * dedicated sending IPs. Results surface in setup-status and on the Health
575
+ * screen. No external API needed — plain DNS lookups.
576
+ */
577
+ dnsbl?: DnsblConfig;
578
+ /**
579
+ * Pulls daily reputation tiers + spam ratio + auth pass rates from Google
580
+ * Postmaster Tools. Trips the (domain × marketing) circuit breaker when a
581
+ * domain falls to reputation `BAD`. Only meaningful at >100/day to Gmail.
582
+ */
583
+ postmaster?: PostmasterConfig;
584
+ /**
585
+ * Pulls daily Outlook/Hotmail/Live IP reputation from Microsoft's Smart
586
+ * Network Data Services. IP-level rather than domain-level — only useful
587
+ * if you send from a dedicated IP. Visibility-only: RED filter results
588
+ * surface as a setup-status error, no auto-trip.
589
+ */
590
+ snds?: SndsConfig;
591
+ /**
592
+ * Optional Mail-Tester integration. When set, the template editor exposes
593
+ * a "Run deliverability check" button that sends a copy of the rendered
594
+ * draft to a Mail-Tester address and polls for a score (0-10) covering
595
+ * SpamAssassin verdict, auth alignment, content red flags, and link
596
+ * reputation. Score < minScore blocks publish until re-run or overridden.
597
+ */
598
+ mailTester?: MailTesterConfig;
599
+ /**
600
+ * DMARC RUA aggregate reports tell you who is sending mail that claims to
601
+ * be from your domain — both legitimate sources and spoofers. Operator
602
+ * uploads the report attachments (or POSTs them via SendGrid Inbound Parse
603
+ * to /admin/mailer/api/dmarc/upload). Mailery decompresses, parses, and
604
+ * surfaces alignment failures + unknown senders.
605
+ */
606
+ dmarc?: DmarcConfig;
484
607
  broadcastConfirmationThreshold?: number;
485
608
  broadcastEnqueueBatchSize?: number;
486
609
  broadcastEnqueueMaxWaiting?: number;
@@ -834,8 +957,22 @@ interface WebhookEventDoc {
834
957
  processed: boolean;
835
958
  raw: unknown;
836
959
  }
960
+ /**
961
+ * Health bucket. One doc per (senderDomain, kind) pair plus a single aggregate
962
+ * doc with _id='agg' that rolls up everything.
963
+ *
964
+ * _id: 'agg' // cross-domain, cross-kind roll-up
965
+ * _id: 'd:<senderDomain>|k:<kind>' // per-bucket
966
+ *
967
+ * The aggregate is informational only — it never trips. Trips live on
968
+ * buckets so one subdomain tanking doesn't hold mail for the others.
969
+ */
837
970
  interface HealthDoc {
838
- _id: 'singleton';
971
+ _id: string;
972
+ /** null on the aggregate doc, set on bucket docs. */
973
+ senderDomain: string | null;
974
+ /** null on the aggregate doc, set on bucket docs. */
975
+ kind: TemplateKind | null;
839
976
  windowStartedAt: Date;
840
977
  windowDurationMs: number;
841
978
  counters: {
@@ -859,6 +996,167 @@ interface HealthDoc {
859
996
  manuallyResumedAt: Date | null;
860
997
  updatedAt: Date;
861
998
  }
999
+ type PostmasterReputation = 'HIGH' | 'MEDIUM' | 'LOW' | 'BAD' | 'REPUTATION_CATEGORY_UNSPECIFIED';
1000
+ /**
1001
+ * One day's traffic stats for a domain from Google Postmaster Tools.
1002
+ * Stored per (domain, date) so we can chart trends.
1003
+ */
1004
+ interface PostmasterSnapshotDoc {
1005
+ _id?: ObjectId;
1006
+ domain: string;
1007
+ /** Reporting date in YYYY-MM-DD per Postmaster's stat day. */
1008
+ date: string;
1009
+ domainReputation: PostmasterReputation | null;
1010
+ ipReputations: Array<{
1011
+ reputation: PostmasterReputation;
1012
+ ipCount: number;
1013
+ }> | null;
1014
+ userReportedSpamRatio: number | null;
1015
+ spfSuccessRatio: number | null;
1016
+ dkimSuccessRatio: number | null;
1017
+ dmarcSuccessRatio: number | null;
1018
+ outboundEncryptionRatio: number | null;
1019
+ inboundEncryptionRatio: number | null;
1020
+ deliveryErrors: Array<{
1021
+ errorType: string;
1022
+ errorClass: string;
1023
+ errorRatio: number;
1024
+ }> | null;
1025
+ spammyFeedbackLoops: Array<{
1026
+ id: string;
1027
+ spamRatio: number;
1028
+ }> | null;
1029
+ fetchedAt: Date;
1030
+ }
1031
+ interface MailTesterFeedback {
1032
+ category: string;
1033
+ severity: 'info' | 'warning' | 'error';
1034
+ message: string;
1035
+ }
1036
+ /**
1037
+ * Cached Mail-Tester deliverability score for a given (template content)
1038
+ * fingerprint. Operators don't burn a credit re-running on identical
1039
+ * content within cacheHours.
1040
+ */
1041
+ interface MailTesterScoreDoc {
1042
+ _id?: ObjectId;
1043
+ templateSlug: string;
1044
+ /** Hash of bodyHash + subject + fromEmail — the cache key. */
1045
+ contentKey: string;
1046
+ checkId: string;
1047
+ score: number;
1048
+ feedback: MailTesterFeedback[];
1049
+ rawSummary: string | null;
1050
+ fetchedAt: Date;
1051
+ expiresAt: Date;
1052
+ }
1053
+ type DmarcPolicy = 'none' | 'quarantine' | 'reject';
1054
+ type DmarcAuthResult = 'pass' | 'fail' | 'softfail' | 'neutral' | 'temperror' | 'permerror' | 'none' | 'unknown';
1055
+ /**
1056
+ * One DMARC RUA aggregate report. Receivers (Google, Yahoo, Microsoft, etc.)
1057
+ * email one per day per sending domain. We store one DmarcReportDoc per
1058
+ * incoming report, plus one DmarcFailureDoc per non-aligned record row.
1059
+ */
1060
+ interface DmarcReportDoc {
1061
+ _id?: ObjectId;
1062
+ reportId: string;
1063
+ /** Reporting org, e.g. 'google.com', 'enterprise.protection.outlook.com'. */
1064
+ orgName: string;
1065
+ /** Reporter contact email from <report_metadata>. */
1066
+ email: string;
1067
+ /** The domain we publish DMARC for, from <policy_published><domain>. */
1068
+ domain: string;
1069
+ policyP: DmarcPolicy;
1070
+ /** % of mail subject to the policy (1-100). */
1071
+ policyPct: number;
1072
+ /** Activity window the report covers. */
1073
+ rangeStart: Date;
1074
+ rangeEnd: Date;
1075
+ /** Sum of <count> across all records. */
1076
+ totalMessages: number;
1077
+ /** Messages that passed DMARC (DKIM aligned OR SPF aligned). */
1078
+ passCount: number;
1079
+ failCount: number;
1080
+ receivedAt: Date;
1081
+ }
1082
+ /**
1083
+ * One alignment-failing source × day. Composite key `(sourceIp, domain, day)`
1084
+ * lets us roll up "this IP sent N misaligned messages over the past week."
1085
+ * Failures from multiple reports for the same key get counted once per
1086
+ * report (we de-dupe on reportId × sourceIp).
1087
+ */
1088
+ interface DmarcFailureDoc {
1089
+ _id?: ObjectId;
1090
+ reportId: string;
1091
+ domain: string;
1092
+ sourceIp: string;
1093
+ count: number;
1094
+ headerFrom: string;
1095
+ dkimResult: DmarcAuthResult;
1096
+ spfResult: DmarcAuthResult;
1097
+ dispositionApplied: 'none' | 'quarantine' | 'reject' | string;
1098
+ /** YYYY-MM-DD bucket derived from the reporting window's mid-point. */
1099
+ day: string;
1100
+ receivedAt: Date;
1101
+ }
1102
+ /**
1103
+ * Operator-set DMARC source tag. Created/updated/deleted from the admin UI
1104
+ * so operators can mark "this IP is our SendGrid", "this is Hubspot", etc.
1105
+ * Merged with `MailerConfig.dmarc.knownSources` at read time — the config
1106
+ * version is read-only baseline, the collection is mutable runtime state.
1107
+ */
1108
+ interface DmarcSourceTagDoc {
1109
+ _id?: ObjectId;
1110
+ ip: string;
1111
+ label: string;
1112
+ ignored: boolean;
1113
+ setBy: string;
1114
+ setAt: Date;
1115
+ }
1116
+ type SndsFilterResult = 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN';
1117
+ /**
1118
+ * One row from a Microsoft SNDS data export covering a single activity
1119
+ * window for one sending IP. Stored unique per (ip, activityStart) so
1120
+ * historical windows accumulate without re-writing.
1121
+ */
1122
+ interface SndsSnapshotDoc {
1123
+ _id?: ObjectId;
1124
+ ip: string;
1125
+ activityStart: Date;
1126
+ activityEnd: Date;
1127
+ rcptCommands: number;
1128
+ dataCommands: number;
1129
+ messageRecipients: number;
1130
+ filterResult: SndsFilterResult;
1131
+ /** 0-1 fraction. SNDS reports as "<0.1%", "0.1-0.3%", etc; we store the upper bound. null when unknown. */
1132
+ complaintRate: number | null;
1133
+ trapMessageCount: number;
1134
+ sampleHelo: string | null;
1135
+ sampleMailFrom: string | null;
1136
+ fetchedAt: Date;
1137
+ }
1138
+ type DnsblResult = 'clean' | 'listed' | 'error';
1139
+ type DnsblTargetKind = 'domain' | 'ip';
1140
+ /**
1141
+ * Latest DNSBL check result per (target, list). Replaced in-place on each run
1142
+ * so the collection stays small. History (if needed later) lives in audit_log.
1143
+ */
1144
+ interface DnsblCheckDoc {
1145
+ _id?: ObjectId;
1146
+ /** Domain (e.g. `mkt.example.com`) or dotted-quad IP (e.g. `203.0.113.5`). */
1147
+ target: string;
1148
+ targetKind: DnsblTargetKind;
1149
+ /** DNS suffix queried (e.g. `dbl.spamhaus.org`). */
1150
+ list: string;
1151
+ /** Display label for the UI (e.g. `Spamhaus DBL`). */
1152
+ listLabel: string;
1153
+ result: DnsblResult;
1154
+ /** A records returned by the lookup, when listed. */
1155
+ returnCodes: string[];
1156
+ /** When result is 'error', the message; otherwise null. */
1157
+ errorMessage: string | null;
1158
+ runAt: Date;
1159
+ }
862
1160
  interface ContactTagDoc {
863
1161
  _id?: ObjectId;
864
1162
  externalId: string;
@@ -883,6 +1181,13 @@ interface Collections {
883
1181
  webhookEvents: Collection<WebhookEventDoc>;
884
1182
  health: Collection<HealthDoc>;
885
1183
  contactTags: Collection<ContactTagDoc>;
1184
+ dnsblChecks: Collection<DnsblCheckDoc>;
1185
+ postmasterSnapshots: Collection<PostmasterSnapshotDoc>;
1186
+ sndsSnapshots: Collection<SndsSnapshotDoc>;
1187
+ dmarcReports: Collection<DmarcReportDoc>;
1188
+ dmarcFailures: Collection<DmarcFailureDoc>;
1189
+ dmarcSourceTags: Collection<DmarcSourceTagDoc>;
1190
+ mailTesterScores: Collection<MailTesterScoreDoc>;
886
1191
  }
887
1192
  declare function getCollections(db: Db, prefix?: string): Collections;
888
1193
  declare function ensureIndexes(db: Db, prefix?: string): Promise<void>;
@@ -902,6 +1207,7 @@ declare class EventRegistry {
902
1207
  register(reg: EventRegistration): void;
903
1208
  has(name: string): boolean;
904
1209
  policy(name: string): DedupePolicy | undefined;
1210
+ list(): EventRegistration[];
905
1211
  /**
906
1212
  * Derive a dedupeKey for an event call. Returns null when no policy is
907
1213
  * registered AND no key was passed — caller should throw.
@@ -922,6 +1228,22 @@ interface RunnerContext {
922
1228
  queues: Queues;
923
1229
  config: ResolvedConfig;
924
1230
  handlebarsHelpers?: Record<string, Handlebars.HelperDelegate>;
1231
+ /**
1232
+ * Optional audit-log writer. Available when the runner context is derived
1233
+ * from a Mailer instance (Mailer.getRunnerContext()); absent in some
1234
+ * lightweight test harnesses. Runner code that uses this must tolerate
1235
+ * `undefined`.
1236
+ */
1237
+ audit?: (entry: {
1238
+ actor: string;
1239
+ action: string;
1240
+ resource: {
1241
+ collection: string;
1242
+ id?: string;
1243
+ slug?: string;
1244
+ };
1245
+ diffSummary?: string;
1246
+ }) => Promise<void>;
925
1247
  }
926
1248
 
927
1249
  /**
@@ -1024,4 +1346,4 @@ declare class NullProvider implements MailProvider {
1024
1346
  reset(): void;
1025
1347
  }
1026
1348
 
1027
- export { type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type SubscriptionStatus as D, type EventDoc as E, type FlowStep as F, type SuppressionDoc as G, type HealthDoc as H, type SuppressionReason as I, type TemplateKind as J, type TemplateVersionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, ensureIndexes as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, getCollections as U, validateSenderDomain as V, type WebhookEventDoc as W, type Contact as a, type SendResult as b, Mailer as c, type SuppressionScope as d, type SegmentFilter as e, type AuditLogDoc as f, type BroadcastStatus as g, type CircuitBreakerThresholds as h, type Collections as i, type ContactTagDoc as j, type FlowDoc as k, type FlowGoal as l, type FlowRunDoc as m, type FlowRunStatus as n, type FlowVersionDoc as o, type HealthStatus as p, type MailerConfig as q, NullProvider as r, type RedisOptions as s, type SegmentDefinition as t, type SendDoc as u, type SendStatus as v, type SenderDomainConfig as w, type SenderDomainRegistry as x, type SenderDomainValidation as y, type SubscriptionDoc as z };
1349
+ export { type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type SubscriptionDoc as D, type EventDoc as E, type FlowStep as F, type SubscriptionStatus as G, type HealthDoc as H, type SuppressionDoc as I, type SuppressionReason as J, type TemplateKind as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type TemplateVersionDoc as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, ensureIndexes as U, getCollections as V, type WebhookEventDoc as W, validateSenderDomain as X, type Contact as a, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type SuppressionScope as e, type SegmentFilter as f, type AuditLogDoc as g, type BroadcastStatus as h, type CircuitBreakerThresholds as i, type Collections as j, type ContactTagDoc as k, type FlowDoc as l, type FlowGoal as m, type FlowRunDoc as n, type FlowRunStatus as o, type FlowVersionDoc as p, type HealthStatus as q, type MailerConfig as r, NullProvider as s, type RedisOptions as t, type SegmentDefinition as u, type SendDoc as v, type SendStatus as w, type SenderDomainConfig as x, type SenderDomainRegistry as y, type SenderDomainValidation as z };