@ryusenkei/mail 0.1.0 → 0.2.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.
- package/README.md +15 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -2
- package/package.json +5 -16
- package/dist/contract.d.ts +0 -6
- package/dist/contract.js +0 -22
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ryusenkei/mail
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The shared shape of an email (`Mail`, `MailAddress`), the `Mailer` interface, and the checks every sender runs before sending. Senders for a provider live elsewhere (for example, `cloudflareMailer` in `@ryusenkei/adapter-cloudflare`); `@ryusenkei/form` uses the interface for its mail destination.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,26 +10,26 @@ pnpm add @ryusenkei/mail
|
|
|
10
10
|
|
|
11
11
|
## Interface
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```ts
|
|
14
|
+
type Mail = { from: MailAddress; to: MailAddress[]; replyTo?: MailAddress; subject: string; text: string; html?: string };
|
|
15
|
+
interface Mailer { send(mail: Mail): Promise<Result<{ id: string | undefined }, 'invalid_input' | 'rejected'>> }
|
|
16
|
+
```
|
|
14
17
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
18
|
+
- `invalid_input`: a malformed address, no recipient, or a line break in a header field (blocks header injection).
|
|
19
|
+
- `rejected`: the provider would not deliver it (a suppressed recipient, a delivery failure).
|
|
20
|
+
- Infrastructure failures are thrown as `TransientError` or `ConfigError` from `@ryusenkei/core`.
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
## Checks
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
- `validateMail(mail)` is what every sender runs before sending. It rejects:
|
|
25
|
+
- CR, LF, NUL, U+0085, U+2028, and U+2029 in the subject or a display name
|
|
26
|
+
- addresses that fail `isEmailAddress`
|
|
27
|
+
- an empty `to`
|
|
28
|
+
- `isEmailAddress(value)` is deliberately loose (not full RFC 5322). It rejects whitespace and `< > ( ) " , ; :`. `@ryusenkei/form` uses the same check, so an address that passes the form is never rejected at send time.
|
|
24
29
|
|
|
25
30
|
## Testing
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
| --- | --- |
|
|
29
|
-
| `@ryusenkei/mail/testing` | `memoryMailer()`: keeps sent mail in `outbox`. For tests and local development; keep it out of production code |
|
|
30
|
-
| `@ryusenkei/mail/contract` | `mailerContract(name, make, mail)`: the behavior every implementation must have, as Vitest tests. Run it against each adapter |
|
|
31
|
-
|
|
32
|
-
The contract imports `vitest`, so it is a separate entry from the in-memory implementation, which local development can use without Vitest.
|
|
32
|
+
`memoryMailer()` from `@ryusenkei/mail/testing` validates, then keeps sent mail in `outbox`.
|
|
33
33
|
|
|
34
34
|
## License
|
|
35
35
|
|
package/dist/index.d.ts
CHANGED
|
@@ -22,5 +22,7 @@ export interface Mailer {
|
|
|
22
22
|
id: string | undefined;
|
|
23
23
|
}, MailError>>;
|
|
24
24
|
}
|
|
25
|
+
/** メールアドレスとして送れる形か(フォームの検査もこれを使い、送信の時に弾かれないようにする) */
|
|
26
|
+
export declare const isEmailAddress: (value: string) => boolean;
|
|
25
27
|
/** 送信の前の入力の検査(実装は送る前にこれを通す) */
|
|
26
28
|
export declare function validateMail(mail: Mail): Result<void, 'invalid_input'>;
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { err, ok } from '@ryusenkei/core';
|
|
2
2
|
// `RFC 5322` の厳密な検査ではなく、明らかな誤りと改行・空白の混入だけを弾く
|
|
3
3
|
const EMAIL = /^[^\s@<>()",;:]+@[^\s@<>()",;:]+\.[^\s@<>()",;:]+$/;
|
|
4
|
-
|
|
4
|
+
// `U+0085`・`U+2028`・`U+2029` も、改行として扱うメールの処理系がある
|
|
5
|
+
const HEADER_BREAK = new RegExp(`[\\r\\n\\u0000${String.fromCharCode(0x85, 0x2028, 0x2029)}]`);
|
|
6
|
+
/** メールアドレスとして送れる形か(フォームの検査もこれを使い、送信の時に弾かれないようにする) */
|
|
7
|
+
export const isEmailAddress = (value) => EMAIL.test(value);
|
|
5
8
|
/** 送信の前の入力の検査(実装は送る前にこれを通す) */
|
|
6
9
|
export function validateMail(mail) {
|
|
7
10
|
const addresses = [mail.from, ...mail.to, ...(mail.replyTo ? [mail.replyTo] : [])];
|
|
8
11
|
if (!mail.to.length)
|
|
9
12
|
return err('invalid_input');
|
|
10
|
-
if (addresses.some((a) => !
|
|
13
|
+
if (addresses.some((a) => !isEmailAddress(a.email) || (a.name !== undefined && HEADER_BREAK.test(a.name))))
|
|
11
14
|
return err('invalid_input');
|
|
12
15
|
if (HEADER_BREAK.test(mail.subject))
|
|
13
16
|
return err('invalid_input');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryusenkei/mail",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "メール送信の型と、宛先・ヘッダーの検査(メモリの実装付き)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RYUSENKEI",
|
|
7
7
|
"repository": {
|
|
@@ -21,28 +21,17 @@
|
|
|
21
21
|
"./testing": {
|
|
22
22
|
"types": "./dist/testing.d.ts",
|
|
23
23
|
"default": "./dist/testing.js"
|
|
24
|
-
},
|
|
25
|
-
"./contract": {
|
|
26
|
-
"types": "./dist/contract.d.ts",
|
|
27
|
-
"default": "./dist/contract.js"
|
|
28
24
|
}
|
|
29
25
|
},
|
|
30
26
|
"files": [
|
|
31
27
|
"dist"
|
|
32
28
|
],
|
|
33
29
|
"publishConfig": {
|
|
34
|
-
"access": "public"
|
|
30
|
+
"access": "public",
|
|
31
|
+
"provenance": true
|
|
35
32
|
},
|
|
36
33
|
"dependencies": {
|
|
37
|
-
"@ryusenkei/core": "^0.
|
|
38
|
-
},
|
|
39
|
-
"peerDependencies": {
|
|
40
|
-
"vitest": ">=3"
|
|
41
|
-
},
|
|
42
|
-
"peerDependenciesMeta": {
|
|
43
|
-
"vitest": {
|
|
44
|
-
"optional": true
|
|
45
|
-
}
|
|
34
|
+
"@ryusenkei/core": "^0.2.0"
|
|
46
35
|
},
|
|
47
36
|
"devDependencies": {
|
|
48
37
|
"typescript": "^5.9.3"
|
package/dist/contract.d.ts
DELETED
package/dist/contract.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
/**
|
|
3
|
-
* 実装が守る契約の試験(Vitest)
|
|
4
|
-
* @param mail 実装が送れる正しいメール(送信元は実装で使える値)
|
|
5
|
-
*/
|
|
6
|
-
export function mailerContract(name, make, mail) {
|
|
7
|
-
describe(`${name}: Mailer の契約`, () => {
|
|
8
|
-
it('正しいメールを送る', async () => {
|
|
9
|
-
expect((await make().send(mail)).ok).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
it('ヘッダーに改行を含むメールは送らずに invalid_input', async () => {
|
|
12
|
-
const mailer = make();
|
|
13
|
-
expect(await mailer.send({ ...mail, subject: 'a\r\nBcc: x@example.com' })).toEqual({ ok: false, error: 'invalid_input' });
|
|
14
|
-
expect(await mailer.send({ ...mail, to: [{ email: 'x@example.com', name: 'a\nb' }] })).toEqual({ ok: false, error: 'invalid_input' });
|
|
15
|
-
});
|
|
16
|
-
it('宛先の形式の誤り・宛先なしは invalid_input', async () => {
|
|
17
|
-
const mailer = make();
|
|
18
|
-
expect(await mailer.send({ ...mail, to: [{ email: 'not-an-address' }] })).toEqual({ ok: false, error: 'invalid_input' });
|
|
19
|
-
expect(await mailer.send({ ...mail, to: [] })).toEqual({ ok: false, error: 'invalid_input' });
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
}
|