@endora-commerce/mod-email 0.100.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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/backend/entities/email-delivery.entity.d.ts +53 -0
- package/dist/backend/entities/email-delivery.entity.d.ts.map +1 -0
- package/dist/backend/entities/email-delivery.entity.js +136 -0
- package/dist/backend/entities/email-delivery.entity.js.map +1 -0
- package/dist/backend/index.d.ts +63 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +43 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/resolve-smtp-url.d.ts +6 -0
- package/dist/backend/resolve-smtp-url.d.ts.map +1 -0
- package/dist/backend/resolve-smtp-url.js +24 -0
- package/dist/backend/resolve-smtp-url.js.map +1 -0
- package/dist/backend/services/email-delivery-recorder.d.ts +19 -0
- package/dist/backend/services/email-delivery-recorder.d.ts.map +1 -0
- package/dist/backend/services/email-delivery-recorder.js +51 -0
- package/dist/backend/services/email-delivery-recorder.js.map +1 -0
- package/dist/backend/services/mailer.d.ts +34 -0
- package/dist/backend/services/mailer.d.ts.map +1 -0
- package/dist/backend/services/mailer.js +29 -0
- package/dist/backend/services/mailer.js.map +1 -0
- package/dist/backend/services/recording-mailer.d.ts +35 -0
- package/dist/backend/services/recording-mailer.d.ts.map +1 -0
- package/dist/backend/services/recording-mailer.js +77 -0
- package/dist/backend/services/recording-mailer.js.map +1 -0
- package/dist/backend/services/smtp-mailer.d.ts +16 -0
- package/dist/backend/services/smtp-mailer.d.ts.map +1 -0
- package/dist/backend/services/smtp-mailer.js +40 -0
- package/dist/backend/services/smtp-mailer.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +190 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260817T070014_email_delivery_record.d.ts +14 -0
- package/dist/migrations/20260817T070014_email_delivery_record.d.ts.map +1 -0
- package/dist/migrations/20260817T070014_email_delivery_record.js +44 -0
- package/dist/migrations/20260817T070014_email_delivery_record.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!-- Generated by `pnpm --filter backend run manifests:generate`. Delete this line to take the file over; a README without it is never regenerated. -->
|
|
2
|
+
|
|
3
|
+
# @endora-commerce/mod-email
|
|
4
|
+
|
|
5
|
+
Mailer abstraction with Console / SMTP drivers for transactional email.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
An **Endora Commerce module package**. Its module id is `email` — the identity of record in the platform's registries, in its migrations and in its setting, permission and translation codes.
|
|
10
|
+
|
|
11
|
+
A module is not imported by application code. The platform discovers the extension packages an instance has installed, reads the manifest on the root subpath and composes the module from it, so installing this package and starting the instance is the whole integration.
|
|
12
|
+
|
|
13
|
+
## Entry points
|
|
14
|
+
|
|
15
|
+
| Import | Contents |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `@endora-commerce/mod-email` | the module manifest — its id, version, dependencies, settings and activation |
|
|
18
|
+
| `@endora-commerce/mod-email/backend` | the composition root the platform calls, with the entities, services, routes and workers it registers |
|
|
19
|
+
| `@endora-commerce/mod-email/migrations` | the module’s own schema migrations, in the order the platform runs them |
|
|
20
|
+
|
|
21
|
+
## Depends on
|
|
22
|
+
|
|
23
|
+
Everything below is a **peer** dependency, so the application resolves exactly one copy of each — two copies of a host or of React are a runtime failure rather than a type error. An entry marked *optional* is needed only by the layers that use it.
|
|
24
|
+
|
|
25
|
+
**Endora packages**
|
|
26
|
+
|
|
27
|
+
- `@endora-commerce/contracts`
|
|
28
|
+
- `@endora-commerce/platform`
|
|
29
|
+
|
|
30
|
+
**Third-party**
|
|
31
|
+
|
|
32
|
+
- `@mikro-orm/core` ^6
|
|
33
|
+
- `@mikro-orm/migrations` ^6
|
|
34
|
+
- `@mikro-orm/postgresql` ^6
|
|
35
|
+
- `nodemailer` ^10
|
|
36
|
+
|
|
37
|
+
## What the tarball carries
|
|
38
|
+
|
|
39
|
+
- `dist/` — the compiled JavaScript and its type declarations
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm add @endora-commerce/mod-email
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Licence
|
|
48
|
+
|
|
49
|
+
MIT — the text is in `LICENSE`, beside this file.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { OptionalProps } from '@mikro-orm/core';
|
|
2
|
+
/**
|
|
3
|
+
* Persistent record of one outbound e-mail decision (D-59, issue #114).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately the `webhook_deliveries` shape rather than a new invention: same
|
|
6
|
+
* problem (an outbound attempt whose outcome nothing survives), same position
|
|
7
|
+
* (written after the attempt, by the seam that made it), same answer (one row
|
|
8
|
+
* per attempt carrying the outcome, no queue, no `pending` state, no row that
|
|
9
|
+
* exists before something was attempted). D-58 refused the transactional outbox
|
|
10
|
+
* for the whole platform; D-59 keeps that refusal for invoice and KSeF document
|
|
11
|
+
* mail and buys the visibility with this row instead.
|
|
12
|
+
*
|
|
13
|
+
* Global rather than org-scoped, for the same reason `webhook_deliveries` is:
|
|
14
|
+
* the transport seam has no tenant — a boot-time send, a worker and an admin
|
|
15
|
+
* request all reach it — and the row is a platform-operator audit trail, read
|
|
16
|
+
* by the person answering "did this leave the building", not by a buyer.
|
|
17
|
+
*/
|
|
18
|
+
export declare class EmailDelivery {
|
|
19
|
+
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'attemptedAt' | 'reason' | 'detail' | 'subject' | 'salesChannelId' | 'documentType' | 'documentId' | 'context';
|
|
20
|
+
id: string;
|
|
21
|
+
/** The caller's idempotency key. Not unique: a resend is a second decision. */
|
|
22
|
+
messageId: string;
|
|
23
|
+
recipient: string;
|
|
24
|
+
/** The transactional-email code, or a legacy in-code builder's own kind. */
|
|
25
|
+
kind: string;
|
|
26
|
+
/**
|
|
27
|
+
* The three answers, and the split is the load-bearing part of this table.
|
|
28
|
+
*
|
|
29
|
+
* `suppressed` is the platform deliberately not sending — the operator
|
|
30
|
+
* switched this e-mail off, or the transport already accepted this message
|
|
31
|
+
* id. `failed` is a message that was meant to go out and did not: the
|
|
32
|
+
* transport raised, none is wired, or no template exists for the code. Since
|
|
33
|
+
* feature 074 shipped per-e-mail deactivation, fusing the two would tell an
|
|
34
|
+
* operator that a configuration they chose is an outage.
|
|
35
|
+
*/
|
|
36
|
+
status: 'sent' | 'suppressed' | 'failed';
|
|
37
|
+
/** Which suppression or which failure, `null` for a delivered message. */
|
|
38
|
+
reason?: string | null;
|
|
39
|
+
/** Free text for the reason — a transport error message, typically. */
|
|
40
|
+
detail?: string | null;
|
|
41
|
+
subject?: string | null;
|
|
42
|
+
/** The channel the message was rendered for, `null` for platform-wide. */
|
|
43
|
+
salesChannelId?: string | null;
|
|
44
|
+
/** The business document delivered, e.g. `invoice` — D-59's whole subject. */
|
|
45
|
+
documentType?: string | null;
|
|
46
|
+
documentId?: string | null;
|
|
47
|
+
/** The sender's `meta` envelope, kept verbatim for the after-the-fact read. */
|
|
48
|
+
context?: Record<string, unknown> | null;
|
|
49
|
+
attemptedAt: Date;
|
|
50
|
+
createdAt: Date;
|
|
51
|
+
updatedAt: Date;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=email-delivery.entity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-delivery.entity.d.ts","sourceRoot":"","sources":["../../../src/backend/entities/email-delivery.entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,aAAa,EAAwB,MAAM,iBAAiB,CAAC;AAIrF;;;;;;;;;;;;;;;GAeG;AACH,qBAEa,aAAa;IACxB,CAAC,aAAa,CAAC,CAAC,EACZ,IAAI,GACJ,WAAW,GACX,WAAW,GACX,aAAa,GACb,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,SAAS,CAAC;IAGd,EAAE,EAAE,MAAM,CAAgB;IAE1B,+EAA+E;IAG/E,SAAS,EAAG,MAAM,CAAC;IAInB,SAAS,EAAG,MAAM,CAAC;IAEnB,4EAA4E;IAG5E,IAAI,EAAG,MAAM,CAAC;IAEd;;;;;;;;;OASG;IAGH,MAAM,EAAG,MAAM,GAAG,YAAY,GAAG,QAAQ,CAAC;IAE1C,0EAA0E;IAE1E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAEvB,uEAAuE;IAEvE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAGvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,0EAA0E;IAE1E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B,8EAA8E;IAG9E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAI7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B,+EAA+E;IAE/E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAIzC,WAAW,EAAE,IAAI,CAAc;IAG/B,SAAS,EAAE,IAAI,CAAc;IAG7B,SAAS,EAAE,IAAI,CAAc;CAC9B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Entity, Index, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
|
11
|
+
import { GlobalEntity } from '@endora-commerce/platform/tenancy';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
/**
|
|
14
|
+
* Persistent record of one outbound e-mail decision (D-59, issue #114).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately the `webhook_deliveries` shape rather than a new invention: same
|
|
17
|
+
* problem (an outbound attempt whose outcome nothing survives), same position
|
|
18
|
+
* (written after the attempt, by the seam that made it), same answer (one row
|
|
19
|
+
* per attempt carrying the outcome, no queue, no `pending` state, no row that
|
|
20
|
+
* exists before something was attempted). D-58 refused the transactional outbox
|
|
21
|
+
* for the whole platform; D-59 keeps that refusal for invoice and KSeF document
|
|
22
|
+
* mail and buys the visibility with this row instead.
|
|
23
|
+
*
|
|
24
|
+
* Global rather than org-scoped, for the same reason `webhook_deliveries` is:
|
|
25
|
+
* the transport seam has no tenant — a boot-time send, a worker and an admin
|
|
26
|
+
* request all reach it — and the row is a platform-operator audit trail, read
|
|
27
|
+
* by the person answering "did this leave the building", not by a buyer.
|
|
28
|
+
*/
|
|
29
|
+
let EmailDelivery = class EmailDelivery {
|
|
30
|
+
[OptionalProps];
|
|
31
|
+
id = randomUUID();
|
|
32
|
+
/** The caller's idempotency key. Not unique: a resend is a second decision. */
|
|
33
|
+
messageId;
|
|
34
|
+
recipient;
|
|
35
|
+
/** The transactional-email code, or a legacy in-code builder's own kind. */
|
|
36
|
+
kind;
|
|
37
|
+
/**
|
|
38
|
+
* The three answers, and the split is the load-bearing part of this table.
|
|
39
|
+
*
|
|
40
|
+
* `suppressed` is the platform deliberately not sending — the operator
|
|
41
|
+
* switched this e-mail off, or the transport already accepted this message
|
|
42
|
+
* id. `failed` is a message that was meant to go out and did not: the
|
|
43
|
+
* transport raised, none is wired, or no template exists for the code. Since
|
|
44
|
+
* feature 074 shipped per-e-mail deactivation, fusing the two would tell an
|
|
45
|
+
* operator that a configuration they chose is an outage.
|
|
46
|
+
*/
|
|
47
|
+
status;
|
|
48
|
+
/** Which suppression or which failure, `null` for a delivered message. */
|
|
49
|
+
reason;
|
|
50
|
+
/** Free text for the reason — a transport error message, typically. */
|
|
51
|
+
detail;
|
|
52
|
+
subject;
|
|
53
|
+
/** The channel the message was rendered for, `null` for platform-wide. */
|
|
54
|
+
salesChannelId;
|
|
55
|
+
/** The business document delivered, e.g. `invoice` — D-59's whole subject. */
|
|
56
|
+
documentType;
|
|
57
|
+
documentId;
|
|
58
|
+
/** The sender's `meta` envelope, kept verbatim for the after-the-fact read. */
|
|
59
|
+
context;
|
|
60
|
+
attemptedAt = new Date();
|
|
61
|
+
createdAt = new Date();
|
|
62
|
+
updatedAt = new Date();
|
|
63
|
+
};
|
|
64
|
+
__decorate([
|
|
65
|
+
PrimaryKey({ type: 'uuid' }),
|
|
66
|
+
__metadata("design:type", String)
|
|
67
|
+
], EmailDelivery.prototype, "id", void 0);
|
|
68
|
+
__decorate([
|
|
69
|
+
Property({ type: 'string', length: 191 }),
|
|
70
|
+
Index(),
|
|
71
|
+
__metadata("design:type", String)
|
|
72
|
+
], EmailDelivery.prototype, "messageId", void 0);
|
|
73
|
+
__decorate([
|
|
74
|
+
Property({ type: 'string', length: 320 }),
|
|
75
|
+
Index(),
|
|
76
|
+
__metadata("design:type", String)
|
|
77
|
+
], EmailDelivery.prototype, "recipient", void 0);
|
|
78
|
+
__decorate([
|
|
79
|
+
Property({ type: 'string', length: 64 }),
|
|
80
|
+
Index(),
|
|
81
|
+
__metadata("design:type", String)
|
|
82
|
+
], EmailDelivery.prototype, "kind", void 0);
|
|
83
|
+
__decorate([
|
|
84
|
+
Property({ type: 'string', length: 16 }),
|
|
85
|
+
Index(),
|
|
86
|
+
__metadata("design:type", String)
|
|
87
|
+
], EmailDelivery.prototype, "status", void 0);
|
|
88
|
+
__decorate([
|
|
89
|
+
Property({ type: 'string', length: 64, nullable: true }),
|
|
90
|
+
__metadata("design:type", Object)
|
|
91
|
+
], EmailDelivery.prototype, "reason", void 0);
|
|
92
|
+
__decorate([
|
|
93
|
+
Property({ type: 'string', length: 4000, nullable: true }),
|
|
94
|
+
__metadata("design:type", Object)
|
|
95
|
+
], EmailDelivery.prototype, "detail", void 0);
|
|
96
|
+
__decorate([
|
|
97
|
+
Property({ type: 'string', length: 998, nullable: true }),
|
|
98
|
+
__metadata("design:type", Object)
|
|
99
|
+
], EmailDelivery.prototype, "subject", void 0);
|
|
100
|
+
__decorate([
|
|
101
|
+
Property({ type: 'uuid', nullable: true }),
|
|
102
|
+
__metadata("design:type", Object)
|
|
103
|
+
], EmailDelivery.prototype, "salesChannelId", void 0);
|
|
104
|
+
__decorate([
|
|
105
|
+
Property({ type: 'string', length: 32, nullable: true }),
|
|
106
|
+
Index(),
|
|
107
|
+
__metadata("design:type", Object)
|
|
108
|
+
], EmailDelivery.prototype, "documentType", void 0);
|
|
109
|
+
__decorate([
|
|
110
|
+
Property({ type: 'string', length: 64, nullable: true }),
|
|
111
|
+
Index(),
|
|
112
|
+
__metadata("design:type", Object)
|
|
113
|
+
], EmailDelivery.prototype, "documentId", void 0);
|
|
114
|
+
__decorate([
|
|
115
|
+
Property({ type: 'json', nullable: true }),
|
|
116
|
+
__metadata("design:type", Object)
|
|
117
|
+
], EmailDelivery.prototype, "context", void 0);
|
|
118
|
+
__decorate([
|
|
119
|
+
Property({ type: 'datetime' }),
|
|
120
|
+
Index(),
|
|
121
|
+
__metadata("design:type", Date)
|
|
122
|
+
], EmailDelivery.prototype, "attemptedAt", void 0);
|
|
123
|
+
__decorate([
|
|
124
|
+
Property({ type: 'datetime', onCreate: () => new Date() }),
|
|
125
|
+
__metadata("design:type", Date)
|
|
126
|
+
], EmailDelivery.prototype, "createdAt", void 0);
|
|
127
|
+
__decorate([
|
|
128
|
+
Property({ type: 'datetime', onUpdate: () => new Date() }),
|
|
129
|
+
__metadata("design:type", Date)
|
|
130
|
+
], EmailDelivery.prototype, "updatedAt", void 0);
|
|
131
|
+
EmailDelivery = __decorate([
|
|
132
|
+
GlobalEntity(),
|
|
133
|
+
Entity({ tableName: 'email_deliveries' })
|
|
134
|
+
], EmailDelivery);
|
|
135
|
+
export { EmailDelivery };
|
|
136
|
+
//# sourceMappingURL=email-delivery.entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-delivery.entity.js","sourceRoot":"","sources":["../../../src/backend/entities/email-delivery.entity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { EmailDeliveryRecorder, EmailMailerPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { ModuleContext } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { EmailDelivery } from './entities/email-delivery.entity.js';
|
|
5
|
+
/**
|
|
6
|
+
* The Email module's backend entry point — converted to feature 072's
|
|
7
|
+
* `registerModule` contract (T079).
|
|
8
|
+
*
|
|
9
|
+
* `email` owns no route and no worker: it is the platform's mailer seam, and
|
|
10
|
+
* everything it contributes is three registrations. Before this conversion the
|
|
11
|
+
* *driver decision* — SMTP when the environment configures one, console
|
|
12
|
+
* otherwise — lived in `composition.ts`, while the test harness and two other
|
|
13
|
+
* modules each carried their own `new ConsoleMailer()` fallback. Four places
|
|
14
|
+
* decided which mailer the platform uses; now the module does.
|
|
15
|
+
*
|
|
16
|
+
* It owns one entity since D-59: the transport is where a message's fate is
|
|
17
|
+
* decided, so it is where the record of that fate belongs.
|
|
18
|
+
*/
|
|
19
|
+
/** What `email` resolves from the container, and the names it owns. */
|
|
20
|
+
export interface EmailCradle {
|
|
21
|
+
/**
|
|
22
|
+
* The nodemailer connection URL, or `null` for the console driver. A
|
|
23
|
+
* registration rather than an inline `process.env` read so a composition
|
|
24
|
+
* root can pin it — the test harness relies on the environment carrying no
|
|
25
|
+
* SMTP configuration, and a name is something it can assert on.
|
|
26
|
+
*/
|
|
27
|
+
readonly emailSmtpUrl: string | null;
|
|
28
|
+
readonly emFactory: () => EntityManager;
|
|
29
|
+
/**
|
|
30
|
+
* The driver — SMTP or console. Separated from `emailMailer` by D-59 so the
|
|
31
|
+
* delivery record wraps *the driver*, which is what makes the record
|
|
32
|
+
* unforgettable: a module resolves `emailMailer` and gets the recording one,
|
|
33
|
+
* with no way to reach the bare transport by accident.
|
|
34
|
+
*/
|
|
35
|
+
readonly emailTransport: EmailMailerPort;
|
|
36
|
+
/** Where every delivery decision is written (D-59). */
|
|
37
|
+
readonly emailDeliveryRecorder: EmailDeliveryRecorder;
|
|
38
|
+
/**
|
|
39
|
+
* The platform's transactional mailer. Consumed by every sending module,
|
|
40
|
+
* against the `EmailMailerPort` contract since feature 075's Phase P.
|
|
41
|
+
*
|
|
42
|
+
* Still `ctx.di.register` rather than `providePort`: this module's manifest
|
|
43
|
+
* declares it non-deactivatable, so a gate on its effective state could never
|
|
44
|
+
* close, and a port advertising a 503 the platform cannot produce is worse
|
|
45
|
+
* than no port. The contract type says the same thing where a consumer reads
|
|
46
|
+
* it — which is the whole of what Phase P changes here.
|
|
47
|
+
*/
|
|
48
|
+
readonly emailMailer: EmailMailerPort;
|
|
49
|
+
}
|
|
50
|
+
export declare function registerModule(ctx: ModuleContext): void;
|
|
51
|
+
/**
|
|
52
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
53
|
+
* array and **no named class export** (D-168).
|
|
54
|
+
*
|
|
55
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
56
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
57
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
58
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
59
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
60
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
61
|
+
*/
|
|
62
|
+
export declare const entities: (typeof EmailDelivery)[];
|
|
63
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACzF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAOtE,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AAEH,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC;IACzC,uDAAuD;IACvD,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IACtD;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiCvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,0BAEpB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveSmtpUrlFromEnv } from './resolve-smtp-url.js';
|
|
2
|
+
import { ConsoleMailer } from './services/mailer.js';
|
|
3
|
+
import { PersistentEmailDeliveryRecorder } from './services/email-delivery-recorder.js';
|
|
4
|
+
import { RecordingMailer } from './services/recording-mailer.js';
|
|
5
|
+
import { SmtpMailer } from './services/smtp-mailer.js';
|
|
6
|
+
import { EmailDelivery } from './entities/email-delivery.entity.js';
|
|
7
|
+
export function registerModule(ctx) {
|
|
8
|
+
ctx.di.register({
|
|
9
|
+
emailSmtpUrl: ctx.asFunction(() => resolveSmtpUrlFromEnv()).singleton(),
|
|
10
|
+
// Singleton, matching what the composition root constructed once at boot:
|
|
11
|
+
// `SmtpMailer` holds a nodemailer transport with its own connection pool,
|
|
12
|
+
// and `ConsoleMailer` deduplicates on `messageId`, so both are wrong as
|
|
13
|
+
// per-resolution instances.
|
|
14
|
+
emailTransport: ctx
|
|
15
|
+
.asFunction(({ emailSmtpUrl }) => emailSmtpUrl ? new SmtpMailer(emailSmtpUrl) : new ConsoleMailer())
|
|
16
|
+
.singleton(),
|
|
17
|
+
// Resolved by `transactional_emails` as well, for the three outcomes it
|
|
18
|
+
// decides before the transport is ever reached. An ordinary registration
|
|
19
|
+
// rather than a `providePort`, exactly like `emailMailer`: `email` is
|
|
20
|
+
// non-deactivatable (074, C3), so a gate over it could never close.
|
|
21
|
+
emailDeliveryRecorder: ctx
|
|
22
|
+
.asFunction(({ emFactory }) => new PersistentEmailDeliveryRecorder(emFactory))
|
|
23
|
+
.singleton(),
|
|
24
|
+
emailMailer: ctx
|
|
25
|
+
.asFunction(({ emailTransport, emailDeliveryRecorder }) => new RecordingMailer(emailTransport, emailDeliveryRecorder))
|
|
26
|
+
.singleton(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
31
|
+
* array and **no named class export** (D-168).
|
|
32
|
+
*
|
|
33
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
34
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
35
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
36
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
37
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
38
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
39
|
+
*/
|
|
40
|
+
export const entities = [
|
|
41
|
+
EmailDelivery,
|
|
42
|
+
];
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAiDpE,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd,YAAY,EAAE,GAAG,CAAC,UAAU,CAAC,GAAkB,EAAE,CAAC,qBAAqB,EAAE,CAAC,CAAC,SAAS,EAAE;QAEtF,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,4BAA4B;QAC5B,cAAc,EAAE,GAAG;aAChB,UAAU,CACT,CAAC,EAAE,YAAY,EAAe,EAAmB,EAAE,CACjD,YAAY,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CACpE;aACA,SAAS,EAAE;QAEd,wEAAwE;QACxE,yEAAyE;QACzE,sEAAsE;QACtE,oEAAoE;QACpE,qBAAqB,EAAE,GAAG;aACvB,UAAU,CACT,CAAC,EAAE,SAAS,EAAe,EAAyB,EAAE,CACpD,IAAI,+BAA+B,CAAC,SAAS,CAAC,CACjD;aACA,SAAS,EAAE;QAEd,WAAW,EAAE,GAAG;aACb,UAAU,CACT,CAAC,EAAE,cAAc,EAAE,qBAAqB,EAAe,EAAmB,EAAE,CAC1E,IAAI,eAAe,CAAC,cAAc,EAAE,qBAAqB,CAAC,CAC7D;aACA,SAAS,EAAE;KACf,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,aAAa;CACd,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nodemailer connection URL: `SMTP_URL` if set, else `SMTP_HOST` + `SMTP_PORT`
|
|
3
|
+
* when not using the console driver (see `backend/.env.example` for Mailhog).
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveSmtpUrlFromEnv(): string | null;
|
|
6
|
+
//# sourceMappingURL=resolve-smtp-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-smtp-url.d.ts","sourceRoot":"","sources":["../../src/backend/resolve-smtp-url.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAoBrD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nodemailer connection URL: `SMTP_URL` if set, else `SMTP_HOST` + `SMTP_PORT`
|
|
3
|
+
* when not using the console driver (see `backend/.env.example` for Mailhog).
|
|
4
|
+
*/
|
|
5
|
+
export function resolveSmtpUrlFromEnv() {
|
|
6
|
+
const explicit = process.env['SMTP_URL']?.trim();
|
|
7
|
+
if (explicit)
|
|
8
|
+
return explicit;
|
|
9
|
+
if (process.env['MAIL_DRIVER'] === 'console') {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const host = process.env['SMTP_HOST']?.trim();
|
|
13
|
+
const port = process.env['SMTP_PORT']?.trim();
|
|
14
|
+
if (host && port) {
|
|
15
|
+
const user = process.env['SMTP_USER']?.trim() ?? '';
|
|
16
|
+
const pass = process.env['SMTP_PASSWORD']?.trim() ?? '';
|
|
17
|
+
if (user) {
|
|
18
|
+
return `smtp://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@${host}:${port}`;
|
|
19
|
+
}
|
|
20
|
+
return `smtp://${host}:${port}`;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=resolve-smtp-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-smtp-url.js","sourceRoot":"","sources":["../../src/backend/resolve-smtp-url.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC;IACjD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,SAAS,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC;IAC9C,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACxD,IAAI,IAAI,EAAE,CAAC;YACT,OAAO,UAAU,kBAAkB,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QAC1F,CAAC;QACD,OAAO,UAAU,IAAI,IAAI,IAAI,EAAE,CAAC;IAClC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { EmailDeliveryRecordInput, EmailDeliveryRecorder } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* Where the delivery record is written (D-59, issue #114).
|
|
5
|
+
*
|
|
6
|
+
* One row per delivery decision, written after the decision — never before,
|
|
7
|
+
* because a row that exists before a send was attempted is the `pending` state
|
|
8
|
+
* of the outbox D-58 refused, one queue hop from the same problem.
|
|
9
|
+
*/
|
|
10
|
+
export type { EmailDeliveryReason, EmailDeliveryRecordInput, EmailDeliveryRecorder, EmailDeliveryStatus, } from '@endora-commerce/contracts';
|
|
11
|
+
/** Where a record that could not be written is reported. Injectable for tests. */
|
|
12
|
+
export type EmailDeliveryLog = (message: string, context: Record<string, unknown>) => void;
|
|
13
|
+
export declare class PersistentEmailDeliveryRecorder implements EmailDeliveryRecorder {
|
|
14
|
+
private readonly emFactory;
|
|
15
|
+
private readonly log;
|
|
16
|
+
constructor(emFactory: () => EntityManager, log?: EmailDeliveryLog);
|
|
17
|
+
record(input: EmailDeliveryRecordInput): Promise<string | null>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=email-delivery-recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-delivery-recorder.d.ts","sourceRoot":"","sources":["../../../src/backend/services/email-delivery-recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGlG;;;;;;GAMG;AAOH,YAAY,EACV,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AAEpC,kFAAkF;AAClF,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE3F,qBAAa,+BAAgC,YAAW,qBAAqB;IAIzE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAH5B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAmB;gBAGpB,SAAS,EAAE,MAAM,aAAa,EAC/C,GAAG,CAAC,EAAE,gBAAgB;IAKlB,MAAM,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAuCtE"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { EmailDelivery } from '../entities/email-delivery.entity.js';
|
|
3
|
+
export class PersistentEmailDeliveryRecorder {
|
|
4
|
+
emFactory;
|
|
5
|
+
log;
|
|
6
|
+
constructor(emFactory, log) {
|
|
7
|
+
this.emFactory = emFactory;
|
|
8
|
+
this.log = log ?? ((message, context) => console.warn(message, context));
|
|
9
|
+
}
|
|
10
|
+
async record(input) {
|
|
11
|
+
// command-coverage-ignore: bookkeeping about a message that has already been
|
|
12
|
+
// sent or suppressed — there is no domain state here to undo, and the write
|
|
13
|
+
// that triggered the message is audited where it happened.
|
|
14
|
+
try {
|
|
15
|
+
const em = this.emFactory();
|
|
16
|
+
const row = em.create(EmailDelivery, {
|
|
17
|
+
messageId: input.messageId,
|
|
18
|
+
recipient: input.recipient,
|
|
19
|
+
kind: input.kind,
|
|
20
|
+
status: input.status,
|
|
21
|
+
attemptedAt: new Date(),
|
|
22
|
+
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
23
|
+
...(input.detail !== undefined ? { detail: input.detail.slice(0, 4000) } : {}),
|
|
24
|
+
...(input.subject !== undefined ? { subject: input.subject.slice(0, 998) } : {}),
|
|
25
|
+
...(input.salesChannelId !== undefined ? { salesChannelId: input.salesChannelId } : {}),
|
|
26
|
+
...(input.documentType !== undefined ? { documentType: input.documentType } : {}),
|
|
27
|
+
...(input.documentId !== undefined ? { documentId: input.documentId } : {}),
|
|
28
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
29
|
+
});
|
|
30
|
+
await em.persistAndFlush(row);
|
|
31
|
+
return row.id;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
// A switched-off module is a presence answer and travels: this recorder is
|
|
35
|
+
// reached across a module boundary, and flattening a 503 into "the record
|
|
36
|
+
// was not written" would turn fail-closed into fail-open for the caller.
|
|
37
|
+
rethrowIfModuleDisabled(error);
|
|
38
|
+
// Everything else is contained, and this is the narrow tolerance the
|
|
39
|
+
// ordering in `RecordingMailer` depends on: the message has already left
|
|
40
|
+
// (or already been suppressed), so a bookkeeping failure must cost the
|
|
41
|
+
// record and nothing else. It is named rather than silent.
|
|
42
|
+
this.log('[email] the delivery record was not written', {
|
|
43
|
+
messageId: input.messageId,
|
|
44
|
+
status: input.status,
|
|
45
|
+
error: error instanceof Error ? error.message : error,
|
|
46
|
+
});
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=email-delivery-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-delivery-recorder.js","sourceRoot":"","sources":["../../../src/backend/services/email-delivery-recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EmailMailerPort, EmailMailerSendInput, EmailMailerSendOutcome, EmailMailerSuppressionReason } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Mailer abstraction (T136 + T178 helper).
|
|
4
|
+
*
|
|
5
|
+
* Every send is idempotent on the message id supplied by the caller — that
|
|
6
|
+
* lets retry loops re-send the same template without duplicating mail.
|
|
7
|
+
*
|
|
8
|
+
* The default `ConsoleMailer` writes to stdout so dev environments work
|
|
9
|
+
* without an SMTP server. Production composition wires a real SMTP
|
|
10
|
+
* transport (e.g. via nodemailer) by implementing this interface.
|
|
11
|
+
*
|
|
12
|
+
* The four shapes moved to `@endora-commerce/contracts` in feature 075's Phase P —
|
|
13
|
+
* thirty-two sites across eight modules named them at this path, and D-59's
|
|
14
|
+
* `MailerSendOutcome` is read one layer up by `transactional_emails`. They are
|
|
15
|
+
* aliased back here so the drivers below and the consumers Phase C has not
|
|
16
|
+
* reached yet keep compiling; the aliases go with the last of those consumers.
|
|
17
|
+
*/
|
|
18
|
+
export type MailerSendInput = EmailMailerSendInput;
|
|
19
|
+
export type MailerSuppressionReason = EmailMailerSuppressionReason;
|
|
20
|
+
export type MailerSendOutcome = EmailMailerSendOutcome;
|
|
21
|
+
export type Mailer = EmailMailerPort;
|
|
22
|
+
export declare class ConsoleMailer implements Mailer {
|
|
23
|
+
private readonly seen;
|
|
24
|
+
send(input: MailerSendInput): Promise<MailerSendOutcome>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* In-memory mailer for tests. Captures every sent message; tests assert
|
|
28
|
+
* against `sent`.
|
|
29
|
+
*/
|
|
30
|
+
export declare class InMemoryMailer implements Mailer {
|
|
31
|
+
readonly sent: MailerSendInput[];
|
|
32
|
+
send(input: MailerSendInput): Promise<MailerSendOutcome>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=mailer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailer.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mailer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,4BAA4B,EAC7B,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAEnD,MAAM,MAAM,uBAAuB,GAAG,4BAA4B,CAAC;AAEnE,MAAM,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAEvD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAC;AAErC,qBAAa,aAAc,YAAW,MAAM;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAEpC,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAe/D;AAED;;;GAGG;AACH,qBAAa,cAAe,YAAW,MAAM;IAC3C,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,CAAM;IAEhC,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAI/D"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class ConsoleMailer {
|
|
2
|
+
seen = new Set();
|
|
3
|
+
async send(input) {
|
|
4
|
+
if (this.seen.has(input.messageId))
|
|
5
|
+
return { status: 'suppressed', reason: 'duplicate_message_id' };
|
|
6
|
+
this.seen.add(input.messageId);
|
|
7
|
+
// eslint-disable-next-line no-console
|
|
8
|
+
console.log(JSON.stringify({
|
|
9
|
+
msg: 'console-mailer',
|
|
10
|
+
to: input.to,
|
|
11
|
+
subject: input.subject,
|
|
12
|
+
body: input.text,
|
|
13
|
+
meta: input.meta,
|
|
14
|
+
}));
|
|
15
|
+
return { status: 'sent' };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* In-memory mailer for tests. Captures every sent message; tests assert
|
|
20
|
+
* against `sent`.
|
|
21
|
+
*/
|
|
22
|
+
export class InMemoryMailer {
|
|
23
|
+
sent = [];
|
|
24
|
+
async send(input) {
|
|
25
|
+
this.sent.push(input);
|
|
26
|
+
return { status: 'sent' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=mailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailer.js","sourceRoot":"","sources":["../../../src/backend/services/mailer.ts"],"names":[],"mappings":"AAgCA,MAAM,OAAO,aAAa;IACP,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,KAAK,CAAC,IAAI,CAAC,KAAsB;QAC/B,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;QACpG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC/B,sCAAsC;QACtC,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CAAC;YACb,GAAG,EAAE,gBAAgB;YACrB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;SACjB,CAAC,CACH,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,cAAc;IAChB,IAAI,GAAsB,EAAE,CAAC;IAEtC,KAAK,CAAC,IAAI,CAAC,KAAsB;QAC/B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC;CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Mailer, MailerSendInput, MailerSendOutcome } from './mailer.js';
|
|
2
|
+
import type { EmailDeliveryRecorder } from './email-delivery-recorder.js';
|
|
3
|
+
/**
|
|
4
|
+
* The mailer every sending module resolves: a driver plus its delivery record
|
|
5
|
+
* (D-59, issue #114).
|
|
6
|
+
*
|
|
7
|
+
* A decorator rather than a line in each of the seventeen call sites, for the
|
|
8
|
+
* reason `providePort`'s gate is a wrapper rather than a call: a record the
|
|
9
|
+
* transport seam writes cannot be forgotten by the module somebody adds next
|
|
10
|
+
* month, and a hand-placed one demonstrably was — that is how "did the customer
|
|
11
|
+
* get the invoice" ended up answerable only from a log.
|
|
12
|
+
*
|
|
13
|
+
* **Ordering, which is the part with a wrong answer.** The transport is called
|
|
14
|
+
* first and the record written after it, in both directions:
|
|
15
|
+
*
|
|
16
|
+
* - a send that succeeded is recorded, and if that write fails the recorder
|
|
17
|
+
* answers `null` (it never throws) so the caller still sees `sent` — the
|
|
18
|
+
* message did leave, and reporting otherwise would be a lie that could
|
|
19
|
+
* trigger a resend;
|
|
20
|
+
* - a send that raised is recorded as `failed` and the error is **re-thrown
|
|
21
|
+
* unchanged**, so every caller's own tolerance keeps deciding what a failed
|
|
22
|
+
* message means for the operation it followed.
|
|
23
|
+
*
|
|
24
|
+
* The other order — record first, then send — is the outbox D-58 refused: it
|
|
25
|
+
* buys atomicity between a business write and an enqueue that nothing here
|
|
26
|
+
* promises, and it costs a row that claims a message was attempted before
|
|
27
|
+
* anything was.
|
|
28
|
+
*/
|
|
29
|
+
export declare class RecordingMailer implements Mailer {
|
|
30
|
+
private readonly transport;
|
|
31
|
+
private readonly recorder;
|
|
32
|
+
constructor(transport: Mailer, recorder: EmailDeliveryRecorder);
|
|
33
|
+
send(input: MailerSendInput): Promise<MailerSendOutcome>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=recording-mailer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-mailer.d.ts","sourceRoot":"","sources":["../../../src/backend/services/recording-mailer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,KAAK,EAAE,qBAAqB,EAA4B,MAAM,8BAA8B,CAAC;AAEpG;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,eAAgB,YAAW,MAAM;IAE1C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,qBAAqB;IAG5C,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAsB/D"}
|