@remit/smtp-service 0.0.16 → 0.0.17
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/smtp-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -16,14 +16,13 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test:typecheck": "tsgo --noEmit",
|
|
18
18
|
"test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=95 --test 'src/**/*.test.ts'",
|
|
19
|
-
"test": "npm run test:typecheck && npm run test:run"
|
|
20
|
-
"test:integ": "RUN_INTEG_TESTS=true node --env-file=../../localhost-dev-aws.env --import tsx --test --test-force-exit 'src/**/*.integ.test.ts'"
|
|
19
|
+
"test": "npm run test:typecheck && npm run test:run"
|
|
21
20
|
},
|
|
22
21
|
"devDependencies": {
|
|
23
22
|
"@types/node": "*"
|
|
24
23
|
},
|
|
25
24
|
"dependencies": {
|
|
26
|
-
"nodemailer": "^
|
|
25
|
+
"nodemailer": "^10.0.6",
|
|
27
26
|
"@remit/data-ports": "*",
|
|
28
27
|
"@types/nodemailer": "*"
|
|
29
28
|
},
|
|
@@ -99,6 +99,17 @@ describe("buildMailMessage", () => {
|
|
|
99
99
|
assert.equal(message.inReplyTo, "<parent@example.com>");
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it("puts exactly one pair of brackets on an id stored with its own", () => {
|
|
103
|
+
const message = buildMailMessage(
|
|
104
|
+
baseOutbox({
|
|
105
|
+
inReplyTo: "<parent@example.com>",
|
|
106
|
+
references: ["<<root@example.com>>", "<parent@example.com>"],
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
assert.equal(message.inReplyTo, "<parent@example.com>");
|
|
110
|
+
assert.equal(message.references, "<root@example.com> <parent@example.com>");
|
|
111
|
+
});
|
|
112
|
+
|
|
102
113
|
it("leaves inReplyTo undefined when absent", () => {
|
|
103
114
|
const message = buildMailMessage(baseOutbox({ inReplyTo: undefined }));
|
|
104
115
|
assert.equal(message.inReplyTo, undefined);
|
package/src/message-builder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OutboxMessageItem } from "@remit/data-ports";
|
|
2
|
+
import { wireMessageId } from "@remit/data-ports/id";
|
|
2
3
|
import nodemailer, { type SendMailOptions } from "nodemailer";
|
|
3
4
|
|
|
4
5
|
export interface MailMessage {
|
|
@@ -55,9 +56,9 @@ export const buildMailMessage = (
|
|
|
55
56
|
subject: outbox.subject,
|
|
56
57
|
text: outbox.textBody,
|
|
57
58
|
html: outbox.htmlBody,
|
|
58
|
-
messageId:
|
|
59
|
-
inReplyTo: outbox.inReplyTo ?
|
|
60
|
-
references: outbox.references?.map(
|
|
59
|
+
messageId: wireMessageId(outbox.messageIdValue),
|
|
60
|
+
inReplyTo: outbox.inReplyTo ? wireMessageId(outbox.inReplyTo) : undefined,
|
|
61
|
+
references: outbox.references?.map(wireMessageId).join(" "),
|
|
61
62
|
// Only the Sent copy has one to carry: the row is stamped after the
|
|
62
63
|
// submission is accepted, so on the wire path this is always absent and
|
|
63
64
|
// nodemailer dates the message as it goes out.
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for SMTP client using mokapi.
|
|
3
|
-
*
|
|
4
|
-
* These tests require mokapi to be running:
|
|
5
|
-
* npm run start:mokapi
|
|
6
|
-
*
|
|
7
|
-
* Run with:
|
|
8
|
-
* npm run test:integ -w packages/smtp-service
|
|
9
|
-
*
|
|
10
|
-
* Note: mokapi SMTP server runs on port 2525 without TLS.
|
|
11
|
-
* Sent emails are delivered to the recipient's IMAP mailbox.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import assert from "node:assert";
|
|
15
|
-
import { describe, test } from "node:test";
|
|
16
|
-
import type { MailMessage } from "./message-builder.js";
|
|
17
|
-
import { generateMessageId } from "./message-id.js";
|
|
18
|
-
import { type SmtpConfig, sendMail } from "./smtp-client.js";
|
|
19
|
-
|
|
20
|
-
const MOKAPI_SMTP_CONFIG: SmtpConfig = {
|
|
21
|
-
host: "localhost",
|
|
22
|
-
port: 2525,
|
|
23
|
-
secure: false,
|
|
24
|
-
user: "alice@mokapi.io",
|
|
25
|
-
credentials: { kind: "password", password: "alice123" },
|
|
26
|
-
tls: {
|
|
27
|
-
rejectUnauthorized: false, // Accept mokapi's self-signed cert
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Generate a unique message ID for testing
|
|
33
|
-
*/
|
|
34
|
-
const createTestMessageId = (): string => {
|
|
35
|
-
return `<${generateMessageId("mokapi.io")}>`;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Create a basic test message
|
|
40
|
-
*/
|
|
41
|
-
const createTestMessage = (
|
|
42
|
-
overrides: Partial<MailMessage> = {},
|
|
43
|
-
): MailMessage => {
|
|
44
|
-
return {
|
|
45
|
-
from: "alice@mokapi.io",
|
|
46
|
-
to: ["bob@mokapi.io"],
|
|
47
|
-
subject: `Test message ${Date.now()}`,
|
|
48
|
-
text: "This is a test message body.",
|
|
49
|
-
messageId: createTestMessageId(),
|
|
50
|
-
...overrides,
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
describe("SMTP client integration tests", {
|
|
55
|
-
skip: !process.env.RUN_INTEG_TESTS,
|
|
56
|
-
}, () => {
|
|
57
|
-
describe("sendMail", () => {
|
|
58
|
-
test("sends a simple text message", async () => {
|
|
59
|
-
const message = createTestMessage({
|
|
60
|
-
subject: `Simple text test ${Date.now()}`,
|
|
61
|
-
text: "Hello from the SMTP integration test!",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
65
|
-
|
|
66
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
67
|
-
assert.equal(result.isTransient, false, "Should not be transient");
|
|
68
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
69
|
-
assert.ok(result.response, "Should return an SMTP response");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("sends an HTML message", async () => {
|
|
73
|
-
const message = createTestMessage({
|
|
74
|
-
subject: `HTML test ${Date.now()}`,
|
|
75
|
-
text: "Plain text fallback",
|
|
76
|
-
html: "<html><body><h1>Hello!</h1><p>This is an <strong>HTML</strong> message.</p></body></html>",
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
80
|
-
|
|
81
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
82
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("sends a message with CC recipients", async () => {
|
|
86
|
-
const message = createTestMessage({
|
|
87
|
-
subject: `CC test ${Date.now()}`,
|
|
88
|
-
to: ["bob@mokapi.io"],
|
|
89
|
-
cc: ["alice@mokapi.io"],
|
|
90
|
-
text: "Message with CC recipient",
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
94
|
-
|
|
95
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
96
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("sends a message with BCC recipients", async () => {
|
|
100
|
-
const message = createTestMessage({
|
|
101
|
-
subject: `BCC test ${Date.now()}`,
|
|
102
|
-
to: ["bob@mokapi.io"],
|
|
103
|
-
bcc: ["alice@mokapi.io"],
|
|
104
|
-
text: "Message with BCC recipient",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
108
|
-
|
|
109
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
110
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("sends a message with multiple recipients", async () => {
|
|
114
|
-
const message = createTestMessage({
|
|
115
|
-
subject: `Multiple recipients test ${Date.now()}`,
|
|
116
|
-
to: ["bob@mokapi.io", "alice@mokapi.io"],
|
|
117
|
-
text: "Message to multiple recipients",
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
121
|
-
|
|
122
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
123
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("sends a reply with In-Reply-To header", async () => {
|
|
127
|
-
const originalMessageId = "<original-message-123@mokapi.io>";
|
|
128
|
-
const message = createTestMessage({
|
|
129
|
-
subject: `Re: Original subject ${Date.now()}`,
|
|
130
|
-
text: "This is a reply to your message.",
|
|
131
|
-
inReplyTo: originalMessageId,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
135
|
-
|
|
136
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
137
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("sends a reply with References header", async () => {
|
|
141
|
-
const references = [
|
|
142
|
-
"<msg-1@mokapi.io>",
|
|
143
|
-
"<msg-2@mokapi.io>",
|
|
144
|
-
"<msg-3@mokapi.io>",
|
|
145
|
-
].join(" ");
|
|
146
|
-
|
|
147
|
-
const message = createTestMessage({
|
|
148
|
-
subject: `Re: Thread subject ${Date.now()}`,
|
|
149
|
-
text: "This is a reply in a thread.",
|
|
150
|
-
inReplyTo: "<msg-3@mokapi.io>",
|
|
151
|
-
references,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
155
|
-
|
|
156
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
157
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("sends a message with Reply-To address", async () => {
|
|
161
|
-
const message = createTestMessage({
|
|
162
|
-
subject: `Reply-To test ${Date.now()}`,
|
|
163
|
-
text: "Please reply to a different address.",
|
|
164
|
-
replyTo: "noreply@mokapi.io",
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
168
|
-
|
|
169
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
170
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("sends a message with formatted From name", async () => {
|
|
174
|
-
const message = createTestMessage({
|
|
175
|
-
from: '"Alice Test" <alice@mokapi.io>',
|
|
176
|
-
subject: `From name test ${Date.now()}`,
|
|
177
|
-
text: "Message with formatted From name",
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const result = await sendMail(MOKAPI_SMTP_CONFIG, message);
|
|
181
|
-
|
|
182
|
-
assert.equal(result.success, true, "Send should succeed");
|
|
183
|
-
assert.ok(result.messageId, "Should return a message ID");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("handles connection failure gracefully", async () => {
|
|
187
|
-
const badConfig: SmtpConfig = {
|
|
188
|
-
host: "localhost",
|
|
189
|
-
port: 9999, // Non-existent port
|
|
190
|
-
secure: false,
|
|
191
|
-
user: "alice@mokapi.io",
|
|
192
|
-
credentials: { kind: "password", password: "alice123" },
|
|
193
|
-
connectionTimeout: 1000, // Short timeout for tests
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const message = createTestMessage();
|
|
197
|
-
const result = await sendMail(badConfig, message);
|
|
198
|
-
|
|
199
|
-
assert.equal(result.success, false, "Send should fail");
|
|
200
|
-
assert.ok(result.error, "Should return an error");
|
|
201
|
-
// Connection errors are typically transient
|
|
202
|
-
assert.equal(
|
|
203
|
-
result.isTransient,
|
|
204
|
-
false,
|
|
205
|
-
"Connection errors without SMTP code are not transient",
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("handles authentication failure", async () => {
|
|
210
|
-
const badAuthConfig: SmtpConfig = {
|
|
211
|
-
host: "localhost",
|
|
212
|
-
port: 2525,
|
|
213
|
-
secure: false,
|
|
214
|
-
user: "alice@mokapi.io",
|
|
215
|
-
credentials: { kind: "password", password: "wrong-password" },
|
|
216
|
-
tls: {
|
|
217
|
-
rejectUnauthorized: false,
|
|
218
|
-
},
|
|
219
|
-
connectionTimeout: 5000,
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const message = createTestMessage();
|
|
223
|
-
const result = await sendMail(badAuthConfig, message);
|
|
224
|
-
|
|
225
|
-
// mokapi may or may not enforce authentication
|
|
226
|
-
// If it does, this should fail; if not, it will succeed
|
|
227
|
-
if (!result.success) {
|
|
228
|
-
assert.ok(result.error, "Should return an error");
|
|
229
|
-
// 5xx errors are permanent (not transient)
|
|
230
|
-
if (result.smtpCode && result.smtpCode >= 500) {
|
|
231
|
-
assert.equal(
|
|
232
|
-
result.isTransient,
|
|
233
|
-
false,
|
|
234
|
-
"Auth failure should be permanent",
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
});
|