npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xtalpi/science-mail-sdk

v0.1.0

Published

Mail SDK for XtalPi Science Space

Readme

@xtalpi/science-mail-sdk

XtalPi Science 客户服务空间邮件投递 SDK。

负责账户激活与密码重置邮件的模板、文案、输入校验、SMTP 传输、超时与传输层重试、错误分类与脱敏。不感知业务规则、部署环境与日志,全部运行参数由构造函数注入。

本包保持为纯 TypeScript 包,不依赖 NestJS、TypeORM、Space App Entity、数据库或对象存储。运行时仅依赖 nodemailer。

安装

pnpm add @xtalpi/science-mail-sdk

使用

import { MailSdk, MailSendError } from '@xtalpi/science-mail-sdk';

const mail = new MailSdk({
  transport: {
    type: 'smtp',
    host: 'smtp.example.com',
    port: 465,
    secure: true,
    username: '[email protected]',
    password: '***',
  },
  from: 'XtalPi Science <[email protected]>',
  timeoutMs: 10_000,
  maxRetries: 1,
});

const result = await mail.sendAccountActivation({
  to: '[email protected]',
  recipientName: 'Alice',
  activationUrl: 'https://space.example.com/activate?token=...',
  expiresAt: new Date(Date.now() + 72 * 3600_000),
  audience: 'member',
});
// result.status === 'sent'

传输方式

构造时通过 transport.type 选择 SMTP 或 Microsoft Graph,其余对外接口一致。

SMTP(见上例)适用于支持 SMTP 的服务商。Microsoft 365 / Exchange Online 推荐用 Graph:

const mail = new MailSdk({
  transport: {
    type: 'graph',
    tenantId: '<entra-tenant-id>',
    clientId: '<app-client-id>',
    clientSecret: '***',
    sender: '[email protected]', // Graph /users/{sender}/sendMail
  },
  from: 'XtalPi Science <[email protected]>',
  timeoutMs: 10_000,
  maxRetries: 1,
});

Graph 方式走 OAuth2 客户端凭证(app-only),需在 Entra 为应用授予 Mail.Send(Application 类型)权限并经管理员同意;该授权属部署接入事项。Graph 发送成功时 messageId 固定为 null。

构造参数(MailSdkConfig)

调用方在 new MailSdk(config) 时注入运行参数;SDK 不读环境变量或配置文件。

| 参数 | 类型 | 说明 | |---|---|---| | transport | SmtpTransportConfig \| GraphTransportConfig | 传输方式,见上节;按 type 区分 | | from | string | 发件人,形如 XtalPi Science <[email protected]> 或纯邮箱;Graph 时地址须与 transport.sender 一致 | | timeoutMs | number | 单次发送/连通性检查超时(毫秒),必填正整数,无默认值 | | maxRetries | number | 传输层可重试错误的额外重试次数,必填正整数;总尝试次数为 1 + maxRetries |

SMTP transport 字段:host、port、secure、username、password。

Graph transport 字段:tenantId、clientId、clientSecret、sender(发信邮箱,对应 Graph /users/{sender}/sendMail)。

配置不完整时构造函数同步抛出 MailSendError(code=invalid_input)。

发送方法与入参

  • sendAccountActivation(input):运营账户邀请、成员邀请及重发。
  • sendPasswordReset(input):两个身份域的密码重置。
  • verifyConnection():验证凭据与连通性,不发送邮件;无入参。

sendAccountActivation / sendPasswordReset 入参

两种方法字段几乎相同,仅链接字段名不同。业务层负责组装完整 URL(含一次性 Token)与过期时刻;SDK 只做校验与渲染。

| 参数 | 激活 | 重置 | 类型 | 约束与语义 | |---|---|---|---|---| | to | ✔ | ✔ | string | 收件人邮箱;合法格式、最长 255。业务层已做小写规范化时,SDK 不改变大小写 | | recipientName | ✔ | ✔ | string | 正文称呼(Hi {name},);去除首尾空白后长度 1–255 | | activationUrl | ✔ | — | string | 完整激活链接;http/https 绝对 URL,最长 2048;渲染为按钮 href 与备用纯文本链接 | | resetUrl | — | ✔ | string | 完整重置链接;约束同 activationUrl | | expiresAt | ✔ | ✔ | Date | 链接有效期截止时刻;须为有效 Date 且不早于调用时刻;邮件中统一渲染为 UTC,如 2026-11-13 09:28 UTC | | audience | ✔ | ✔ | 'operator' \| 'member' | 选择身份域文案变体,不直接出现在正文 |

audience 与场景的组合决定主题与正文措辞(4 套固定变体):

| 方法 | audience | 用途 | |---|---|---| | sendAccountActivation | operator | 平台运营账户邀请/重发 | | sendAccountActivation | member | 租户成员邀请/重发 | | sendPasswordReset | operator | 运营侧密码重置 | | sendPasswordReset | member | 成员侧密码重置 |

入参示例:

await mail.sendAccountActivation({
  to: '[email protected]',
  recipientName: 'Alice',
  activationUrl: 'https://space.example.com/activate?token=...',
  expiresAt: new Date(Date.now() + 72 * 3600_000),
  audience: 'member',
});

await mail.sendPasswordReset({
  to: '[email protected]',
  recipientName: 'Bob',
  resetUrl: 'https://space.example.com/operator/reset?token=...',
  expiresAt: new Date(Date.now() + 3600_000),
  audience: 'operator',
});

入参校验失败(invalid_input)

发送前 SDK 按上表逐项校验;任一字段不满足约束即失败,不会发起 SMTP/Graph 网络请求。抛出的错误固定为:

error instanceof MailSendError
error.code === 'invalid_input'
error.retryable === false
error.message === 'The mail request input is invalid.' // 中性描述,不含具体字段值

| 字段 | 含义 | |---|---| | code | 稳定分类码,供 Space App 写入 dispatch_error_code 或对客户端映射;入参问题一律为 invalid_input | | retryable | 是否值得业务层触发显式重发。入参错误为 false:改对参数前重发没有意义,SDK 传输层也不会因该错误重试 | | message | 已脱敏的英文说明;不含收件人、链接、Token 或哪个字段出错的细节 |

典型失败示例(均得到上述同一组 code / retryable):

| 参数 | 失败情形 | |---|---| | to | 空串、缺 @、含空白的非法格式、长度超过 255 | | recipientName | 仅空白、trim 后为空、trim 后超过 255 | | activationUrl / resetUrl | 相对路径、非 http/https(如 javascript:)、无法解析为 URL、长度超过 2048 | | expiresAt | 非 Date、Invalid Date、时刻早于调用时刻 | | audience | 非 'operator' / 'member' |

业务层处理建议:将 invalid_input 记为投递失败并修正入参后由运营/用户再次触发;不要按可重试依赖故障自动重发。

调用方传参与 SDK 内置内容的边界

调用方只传变量数据,不传主题、正文片段、HTML、多语言、抄送或附件。

| 邮件内容 | 由谁决定 | |---|---| | 收件人、称呼名、行动链接、过期时刻、身份域变体 | 上层传参(上表) | | 发件人展示名与地址、传输凭据、超时与重试 | 构造参数 MailSdkConfig | | 主题行、标题、正文说明、按钮文案、重置安全提示 | SDK 模板(按方法 × audience 选择) | | 品牌名、配色、布局骨架、页脚、UTC 时间格式 | SDK 公共布局(写死) |

成功返回值 MailSentResult

| 字段 | 类型 | 说明 | |---|---|---| | status | 'sent' | 邮件服务已接受发送请求;不表示已进入收件箱或已读 | | messageId | string \| null | 服务商侧消息标识;SMTP 可能有值,Graph 固定为 null | | acceptedAt | Date | 接受发送请求的时间 |

错误处理

发送失败抛出 MailSendError:

try {
  await mail.sendPasswordReset(input);
} catch (error) {
  if (error instanceof MailSendError) {
    // error.code:稳定分类码(见下表)
    // error.retryable:是否值得业务层触发显式重发(业务层不自动重试)
    // error.message:已脱敏中性英文,不含收件人、链接、Token 或凭据
  }
  throw error;
}

| code | retryable | 含义 | |---|---|---| | invalid_input | false | 构造配置或发送入参校验失败 | | authentication_failed | false | SMTP 凭据无效,或 Graph 鉴权/权限失败 | | rate_limited | true | 服务商限流(如 SMTP 421/450/452、Graph 429) | | dependency_unavailable | true | 网络失败、DNS 失败或上游 5xx | | operation_timed_out | true | 单次尝试超过 timeoutMs | | invalid_response | false | 上游响应无法解释 |

retryable=true 时 SDK 会按 maxRetries 在传输层再试;最终仍失败时,业务层仅在人工/用户显式重发场景下再次调用,不自动循环重试。

手动发送(真实收件箱)

用于核对全部模板变体的主题与排版。会向 MAIL_TO 连续发送 4 封:激活/重置 × operator/member。

cp .env.example .env   # 填入真实凭据与收件邮箱
pnpm build
pnpm manual:send

.env 不会被提交。用 MAIL_TRANSPORT 选择传输方式:

  • MAIL_TRANSPORT=smtp(默认):填 MAIL_HOST/MAIL_PORT/MAIL_SECURE/MAIL_USER/MAIL_PASS。常见端口 465 + MAIL_SECURE=true,或 587 + MAIL_SECURE=false(由服务商做 STARTTLS)。
  • MAIL_TRANSPORT=graph:填 MAIL_GRAPH_TENANT_ID/MAIL_GRAPH_CLIENT_ID/MAIL_GRAPH_CLIENT_SECRET/MAIL_GRAPH_SENDER。

开发

pnpm typecheck
pnpm test        # unit + e2e
pnpm test:unit
pnpm test:e2e
pnpm build
pnpm test:smoke  # 针对构建产物
pnpm check       # typecheck + test + build + smoke

构建产物同时支持:

  • ESM:dist/esm/index.js
  • CommonJS:dist/cjs/index.cjs
  • TypeScript 类型声明:dist/esm/index.d.ts

执行 pnpm publish 前会通过 prepublishOnly 自动运行 check。