@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ryusenkei/mail
2
2
 
3
- The port for sending email: the `Mailer` interface, its error codes, an in-memory implementation, and a contract test. No runtime dependencies besides `@ryusenkei/core`. Implementations for a provider live in the `adapter-*` packages.
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
- `send({ from, to, replyTo?, subject, text, html? })` returns the provider message ID.
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
- - Business failures come back as `Result` with these codes: `invalid_input` (malformed address, or a line break in a header field, which blocks header injection), `rejected`
16
- - Infrastructure failures are thrown as `TransientError` (may succeed on retry) or `ConfigError` (a missing setting), from `@ryusenkei/core`
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
- `validateMail(mail)` is the shared input check. Every implementation runs it before sending.
22
+ ## Checks
19
23
 
20
- ```ts
21
- const sent = await services.mailer.send({ from, to: [owner], subject, text });
22
- if (!sent.ok) log('warn', 'mail_rejected', { error: sent.error });
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
- | Entry | Contents |
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
- const HEADER_BREAK = /[\r\n\u0000]/;
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) => !EMAIL.test(a.email) || (a.name !== undefined && HEADER_BREAK.test(a.name))))
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.0",
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.1.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"
@@ -1,6 +0,0 @@
1
- import type { Mail, Mailer } from './index.ts';
2
- /**
3
- * 実装が守る契約の試験(Vitest)
4
- * @param mail 実装が送れる正しいメール(送信元は実装で使える値)
5
- */
6
- export declare function mailerContract(name: string, make: () => Mailer, mail: Mail): void;
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
- }