Repository navigation
Commit 3d830b7
feat(connector-imap, service-mailbox): the IMAP mail source — a read-only IMAP connector and the mailbox sync that feeds the ingest seam (#23303)
Fixes #22871
Clause-②: yes (a new published package, `@objectstack/connector-imap`,
and its exports; the `mailAssociation` liveness row goes `live`)
## Stage 2 of #22871: `connector-imap` feeds the stage-1 ingest seam
Stage 1 (PR #23250, `82581819b`) shipped the ingest seam:
`MailIngestService.ingest` turns one raw RFC 822 message into one
`sys_mail_message` row, with dedupe, threading, association, the pool
and suppression. Nothing fed it. This PR adds the mail source: a new
package, `@objectstack/connector-imap`, and an IMAP sync in
`@objectstack/service-mailbox` that polls each mailbox's folder above a
stored UID cursor and hands every message to the seam. Stage 1's dedupe,
threading, matching, re-match and suppression are reused unchanged
(ruling 6116489638: Q1 A, Q3 C phase 1, Q4 A and Q5 A are not touched).
Attachment extraction, the card's last scope item, moves to #23307
(`Blocked-by: #23306`, the server-side `sys_attachment` writer), under
the seat's ruling B (`6151023438`).
### `@objectstack/connector-imap` (new): protocol and auth only
- `openImapSession({ account, credentials, timeoutMs?, maxLiteralBytes?
})` opens a **read-only** IMAP4rev1 session:
- `examine(folder)` answers `uidValidity`, `uidNext` and `exists`. It
uses `EXAMINE` and never `SELECT`. A folder opened without a UIDVALIDITY
is refused.
- `listMessages(fromUid)` and `fetchMessages(uids)` fetch with
`BODY.PEEK[]`, so no message is ever marked `\Seen`.
- `logout()` and `close()` end the session.
- **The account and credential abstraction is shaped for the per-user
mailbox (decision 4).**
- `MailAccount` is `{ id, username, imap: { host, port?, security? } }`,
one identity per account, never a process-wide setting.
- `MailCredentialProvider.credentialFor(account)` is resolved each time
a session opens and never cached. A per-user provider can therefore read
a user's stored credential and refresh an OAuth token without changing
the connector.
- The endpoint sits under `imap`, so the same account can later name a
calendar endpoint. The design record shares one account between the mail
and calendar engines.
- `staticCredentials(credential)` is the shared mailbox's implementor: a
fixed credential the host hands over.
- **Auth:**
- a `password` uses `AUTHENTICATE PLAIN` (with `SASL-IR` when offered),
otherwise `LOGIN`, with a synchronizing literal when quoting cannot
carry the password;
- an `oauth2` token uses `AUTHENTICATE XOAUTH2`, including its
error-continuation flow;
- a server that announces `LOGINDISABLED` and offers no `PLAIN` is
refused before anything is sent.
- **Transport:** implicit TLS by default, with the certificate verified.
`security: 'none'` is refused to any host but loopback, before a socket
opens.
- **Shape:** it mirrors the existing connectors' package shape:
manifest, `LICENSE`, `tsconfig.json`, `vitest.config.ts` and co-located
tests. It has no runtime dependencies. It registers **no**
`connector_action` handler and ships no plugin, because nothing in a
flow calls IMAP; the mailbox service does.
- `@objectstack/connector-imap/testing` exports `ImapServerDouble`, the
in-process server the tests run against. It is a separate entry with its
own `tsup.config.ts`.
### `@objectstack/service-mailbox`: the sync half
- **Composition:** `new MailboxServicePlugin({ imap: { accountFor,
pollIntervalMs?, batchSize?, maxMessagesPerRun?, maxMessageBytes? } })`
schedules the interval job `mailbox:imap-sync` on the composed job
service at `kernel:ready`. `destroy()` cancels the job and closes any
open session.
- The capability boot constructs the plugin with no options, and then no
mailbox is synced. The boot says so once, at info.
- With `imap` but no job service, the boot warns.
`plugin.syncService.syncAll({ isSystem: true })` is the manual path.
- A run in which a mailbox could not be synced resolves `{ outcome:
'degraded' }` for the job service (the `JobRunOutcome` contract).
- **`MailboxSyncService`, per mailbox:**
- it asks `accountFor(mailbox)` for the account and credential (`null`
means not synced);
- it opens the session, reads the folder's UIDVALIDITY, and lists the
UIDs above the cursor;
- it fetches them `batchSize` at a time and hands each to `ingest` under
the system context, in UID order.
- **The cursor** lives on `sys_mailbox` in four new readonly fields,
written by the sync only: `imap_folder`, `imap_uid_validity`,
`imap_last_uid` and `imap_synced_at`. That is a widening of a published
object, declared in the changeset.
- A cursor taken under another UIDVALIDITY, or in another folder, is
discarded and the folder is read from its start. Messages already stored
answer `duplicate`, so nothing doubles.
- The cursor moves after each batch, past every message stored, already
stored, suppressed or skipped. A store failure stops the run below the
failed message, so the next run retries it.
- A message above `maxMessageBytes` (32 MiB by default), or one the seam
refuses as unreadable, is skipped so it cannot stall the mailbox. It is
logged at `error` once, naming its UID, with the consequence and the
fix.
- One mailbox is never synced twice at once in one process. Two
processes waste a fetch and store nothing twice, because the dedupe key
is unique.
- **No credential is stored on any object.** The account and credential
come from the host's `accountFor`, the way every open connector takes
its credential (measured below). `system-context.mdx` gains row 47c for
the sync's two system-only entry points.
### Stage 1's four carried items (`6145459294`)
1. **`mailAssociation` → `live`** (`packages/spec/liveness/field.json`).
`authorWarn` and `authorHint` are dropped. The
`authorWarnedProperties('field')` pin becomes `['conditionalRequired']`,
and the `field` state counts go from 94 live / 4 planned to 95 / 3. The
evidence anchor is `match-records.ts#MailRecordMatcher` rather than the
dispatch's `#MailRecordMatcher.match`, because `check:liveness` refuses
a dotted anchor ("an anchor must be one identifier"). The prose names
`match`, through `flaggedFieldsOf`. This flip is its own first commit
(`610e112bd`). Under the seat answer `6151023438` it lands first on its
own PR, #23314. This PR keeps the commit, which becomes a no-op once
#23314 is on `main` and `main` is merged here.
2. **Attachment storage:** not built here. It moves to #23307 under
ruling B (`6151023438`). See Measurements, H5.
3. **`message/global` (RFC 6532):** it is now read as an `attachment`
source, like `message/rfc822`, including a base64-encoded part. It is
pinned in `parse-message.test.ts`.
4. **`MAX_FORWARDED_PARTS = 20`** is named in the service README, with
what happens past it.
### Measurements (Zone 2), at the merge base `175e8d414`
- **H1, the IMAP client: an own minimal implementation, zero runtime
dependencies.**
- `imapflow@2.4.0` (MIT, ESM and CJS, 1.75 MB unpacked) pins 8
exact-version dependencies: `pino` with its worker-thread transport,
`socks`, `iconv-lite`, `libmime`, `libqp`, `libbase64`,
`encoding-japanese` and `@zone-eu/mailsplit`. That is a lot for the five
commands a read-only mirror sends.
- `imap@0.8.19` (node-imap) is unmaintained.
- The subset this needs is `CAPABILITY`, `LOGIN` / `AUTHENTICATE`,
`EXAMINE`, `UID FETCH` and `LOGOUT`, plus literal framing. That is about
1,000 lines with the wire reader and the account types, and MIME stays
with stage 1's `postal-mime`.
- **H1, the server double: an own in-process double, plus a one-off
interop probe against an independent strict server.**
- `hoodiecrow-imap` was dormant from 2.1.0 (2016) until 3.x on
2026-10-06/07, and was deprecated hours later as "Renamed to imapkit".
- `imapkit` was created 2026-10-07 and had 11 releases in two days;
5.0.1 is five days old.
- `wildduck` needs MongoDB and Redis.
- So no third-party package is added, even as a dev dependency.
`ImapServerDouble` shares no parser or constant with the client.
- The client was probed once, outside the tree, against **both**
`imapkit@5.0.1` and `hoodiecrow-imap@3.3.1`. Both are strict RFC 3501
servers by the postal-mime maintainers. 18 checks, **all PASS on both**:
- `LOGIN`, `AUTHENTICATE PLAIN` with and without `SASL-IR`, a UTF-8
password, and a password carrying a double quote and a backslash;
- `XOAUTH2` success and its error-continuation refusal, a wrong password
as `authentication`, and `LOGINDISABLED` refused before sending;
- `EXAMINE` with UIDVALIDITY and UIDNEXT, `UID FETCH` sizes, the
last-UID edge case of a range ending in the star, byte-exact 7-bit and
8-bit bodies, and `INTERNALDATE` with an offset;
- a modified UTF-7 folder name, `LOGOUT`, and no `\Seen` left after a
fetch. That last check is read through imapkit's control API; hoodiecrow
has none, so it is NOT MEASURED there.
- **H2, credentials:**
- All four existing connectors take a static credential from the host
composition: `connector-slack` `token`, `connector-rest`
`authentication`, `connector-mcp` env/headers, and the showcase's
`process.env.SLACK_BOT_TOKEN`. "OAuth2 install/refresh, credential
vaulting, and multi-tenant connection lifecycle are the enterprise tier"
is the slack connector's own header.
- The sync reuses that path (`accountFor` plus a credential provider).
No secret store is added, and no `secret` field either. The engine's
encrypted `secret` channel (`resolveSecretField`) is the natural store
for the per-user phase.
- **H3, cursor state:** `sys_mailbox` held no cursor. Its own header
said the account, credential and cursor "are added by the ingest half".
Four fields are added (the widening above). The account and credential
are not stored, by H2.
- **H4, scheduling:** this reuses `IJobService.schedule(name, { type:
'interval' }, handler)` resolved at `kernel:ready`, plus `cancel` in
`destroy()`. That is plugin-approvals' SLA clock pattern. No timer of
its own; `check:plugin-teardown-shape` is green.
- **H5, attachment storage: no server-side writer exists.**
- `sys_attachment` (platform-objects) has 0 non-test `insert` call sites
in the tree.
- `sys_file` is written only by service-storage's
`StorageMetadataStore.createFile`, reached from the upload routes.
- The kernel `storage` service (`IStorageService`) is a blob store with
no file-row or attachment-link verb.
- Storing a mail attachment needs a new platform capability in
service-storage: bytes, the `sys_file` row, and the `sys_attachment`
link, with quota, organization and parent-access semantics. The dispatch
forbids building that here. The seat ruled B (`6151023438`): the writer
is #23306, and the mail half is #23307.
### Hardening round (seat note `6151344448`), commit `4488d1171`
1. **Bounded accumulation.** The session refuses, as the new `ImapError`
kind `oversized`, and closes the connection when:
- one response carries more than `IMAP_MAX_RESPONSE_LITERALS` (32)
literals;
- the untagged responses to one command total more than
`maxCommandBytes` (default 128 MiB; each response is charged 256 bytes
beyond its own size).
The existing line and literal refusals moved from `protocol` to
`oversized`. The package is unreleased, and the changeset lists the
kinds.
2. **`maxLiteralBytes` follows `maxMessageBytes`.** The sync opens the
session with `maxLiteralBytes = maxMessageBytes +
SYNC_LITERAL_ENVELOPE_BYTES` (64 KiB), and `maxCommandBytes = max(128
MiB, batch budget + maxMessageBytes + envelope)`.
- An `oversized` refusal during a batch fetch reopens the session
(checking UIDVALIDITY), re-reads the batch one UID at a time, and skips
the offender as `too_large`.
- A body that arrives within the envelope but above the bound is skipped
too.
- The cursor moves past a skipped message, and the next message lands.
3. **A batch byte budget.** Batches are bounded by `maxBatchBytes`
(default 64 MiB, never below one message's bound) as well as by count. A
message the listing says is oversized costs 0, because it is never
fetched; an unsized one costs the whole bound.
4. **The `\Seen` sentence** in `testing.ts`'s header and in the
connector README now says what the code does: only `BODY[]` under
`SELECT` marks a message seen, and `EXAMINE` never changes state.
5. **The changeset** drops `'@objectstack/spec': patch` and the spec
bullet, because #23314 carries them. It also drops the liveness clause
from its own `Clause-②: yes` line. The `llms.txt` package count (70 to
71) rides the fixed-group release.
`main` was merged in via `os-regen-merge.sh` (`c33c6dc67`), because
GitHub reported a census conflict. `main`'s side was taken for the two
census files, which were then regenerated.
### Fix round after the delta review FAIL (`6154709411`), commits
`a11f7c57e`, `2725aacca`, `5e075ec2f`
1. **The cursor passes every skipped message.** `skip()` now advances
`result.lastUid` itself, so all three `too_large` paths pass the cursor,
including a message that is the last of its batch and run. Four
offender-last pins cover: listed above the bound; refused alone with the
size understated; refused alone with the size omitted; above the bound
once read. The second run fetches nothing and logs nothing new.
2. **A literal is charged at its announcement.** At each `{n}`
announcement, before any octet is read, the reader charges the running
response against `maxCommandBytes`. One hostile response now holds at
most `maxCommandBytes` plus one line. A pin uses the double's new
`withheld-literal` mode.
3. **The changeset's `openImapSession` line** gains `maxCommandBytes?`.
4. **The service README names the listing ceiling.** At the default
bound, a folder with more than about 450,000 messages above the cursor
is refused on the listing. A host raises the bound through
`maxBatchBytes`; paging is the per-user phase's.
Carried, not fixed: a run's time is unbounded under a server that
trickles a literal. Memory stays bounded. Carrier: the per-user mailbox
phase of #22871.
`main` was merged in again (`671693ad6`) after PR #23314 landed as
`777eed2bc`. The one conflict, the `mailAssociation` row's `note`,
resolved to `main`'s side. This PR's liveness and lint-test files now
equal `main`'s, so `610e112bd` is a no-op.
### Size and landing
47 files, +4105 / -63 = **4168 changed lines against the merge base**
(`LICENSE` is 202 of them). That is over the 3,000-line threshold, so
under the seat answer `6151023438` this PR takes the Tier H route: the
seat's ACCEPT and an at-tier contract review, then it waits for a human
merge. The seat does not arm auto-merge. The diff touches no governed
surface.
### Verification at `671693ad6` (merged with `origin/main` at
`430e5f5ad`)
The figures at `671693ad6` come first, then those at `44aabc877`, then
the reading at `27aa15166`, on which the first contract review PASSed
(`6151327338`).
- **At `671693ad6`** (under `os-verify-lock`):
- a full build, 75 of 75;
- `connector-imap` 42 passed; `service-mailbox` 101 passed; both
typechecks exit 0;
- `spec` `check:liveness` exit 0, and the lint liveness pin 100 passed;
- ablations F1 (no cursor advance in `skip()`: 4 red) and F2 (no
per-announcement charge: 1 red), each restored to blob == HEAD;
- the interop run on the new dist: `imapkit@5.0.1` and
`hoodiecrow-imap@3.3.1`, 18 of 18 each;
- `dispatch-gates --ran`: 130 derived, 130 run, all exit 0.
- **At `44aabc877`** (under `os-verify-lock`):
- a full turbo build, 75 of 75 (the first post-merge build was
OOM-killed under contention; only the green re-run is cited);
- `spec` `check:generated`, all 14 current;
- `@objectstack/connector-imap`: 2 files, 41 passed;
`@objectstack/service-mailbox`: 8 files, 97 passed; both typechecks exit
0;
- six more ablations (N1 to N6: the literals bound, the command bound,
`maxLiteralBytes` not passed, no one-at-a-time re-read, no byte budget,
no post-fetch length check), each red as predicted and restored to blob
== HEAD;
- the interop run on the hardened dist: `imapkit@5.0.1` and
`hoodiecrow-imap@3.3.1`, 18 of 18 each;
- `dispatch-gates --ran`: 132 derived, 132 run; 131 exit 0, and
`check:platform-checklist` exits 1 with `main`'s 8 findings;
- ESLint over the hardening round's 12 changed lintable files: 0 errors,
0 warnings;
- `spec` `test:repo` was not re-run, because the hardening touches no
spec file.
- **At `27aa15166`:**
- **Suites:**
- `@objectstack/connector-imap`: 2 files, 37 tests passed.
- `@objectstack/service-mailbox`: 8 files, 91 tests passed (stage 1 had
64). These run on a real ObjectQL over driver-sql and better-sqlite3
`:memory:`, plus a booted `ObjectKernel`.
- `@objectstack/lint` `lint-liveness-properties.test.ts`: 100 passed.
- `pnpm --filter @objectstack/spec test:repo`: 58 files, 1007 passed.
- **Typecheck:** both packages exit 0. `tsc --listFiles` counts 2 of 2
and 8 of 8 test files.
- **Ablations:** 11, each through `scripts/ablation-replace.mjs`. Each
anchor hit once, each blob change was proven on disk, each file was
restored to blob == HEAD with `git diff HEAD` empty, and the tests ran
on mutated source (vitest aliases every workspace import to `src`). All
11 went red as predicted:
- clause 1: the sync hands nothing to ingest — 14 tests red;
- clause 2: the cursor is never applied (5), the dedupe key differs per
fetch (2), and a changed UIDVALIDITY keeps the old cursor (1);
- clause 3: re-match adds no link — 1;
- clause 4: suppression removed — 1;
- read-only: `BODY[]` instead of `BODY.PEEK[]` (1), and `SELECT` instead
of `EXAMINE` (1);
- plaintext to a non-loopback host accepted — 1;
- `message/global` not read — 2;
- the cursor never saved — 5.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 132 families on this head. All 132
were run, one log each, and `--ran` reconciles: "132 derived, 132 run, 0
NOT-MEASURED, 0 UNRUN".
- 131 exit 0, among them `check:dual-build-cjs-loads`,
`check:published-files`, `check:i18n`, `check:system-context-census`,
`check:tenant-audit-census`, `check:lockstep-package-count`,
`check:changeset-no-major` and `check:generated` after a real spec
build.
- `pnpm check:platform-checklist` exits 1 with 8 findings, the same 8 as
on the merge base (Acceptance notes).
- Extra, not derived: `pnpm check:i18n-coverage` exits 0, with none new.
- The tree is 3 commits behind `origin/main` `9c7cfbf0a`. Those commits
touch none of this diff's files. One of them changes
`scripts/engine-double-contract.pinned.json`, which the derivation
reads; its gate ran green on this tree's copy.
- **Lint (narrowed):** `eslint --no-inline-config --format json` over
the 33 changed lintable files gives 33 files, 0 errors, 0 warnings, 0
fatal.
- The population is `eslint.config.mjs` files
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`; the rest of the diff is md, mdx
and json. The count is read from the JSON.
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`), so no untouched file's verdict can move.
- `turbo ls --affected` is not used for any narrowing claim here.
### Acceptance notes
- **`check:platform-checklist` is red on the merge base itself**
(`175e8d414`, run in a throwaway worktree): the same 8 ABSENT SYMBOL /
SYMBOL ANCHORS LOST findings in
`docs/qa/platform-checklist/areas/{access-security,attachments-storage,search}.json`.
This diff touches none of its inputs. Not fixed here.
- **The stage-1 changeset** (`.changeset/22871-mail-ingest-seam.md`)
still says no mail source feeds the seam. It is not edited; this PR's
changeset says it supersedes that line.
- **The `sys_mailbox` row itself** (organization, address, owner) still
has no administration door. Server code writes it under the system
context, as after stage 1. The README says so.
- **`pnpm-lock.yaml`:** `pnpm install` added the two workspace importer
entries and also flipped one unrelated `vitest` peer variant (esbuild
0.28.2 ↔ 0.28.1), the normalization stage 1 met in the other direction.
The lockfile delta was rebuilt from the base lockfile with only the two
importer entries (15 lines), and `pnpm install --frozen-lockfile`
validates it.
## 维护者速读(草稿)
**改了什么**
- 新增发布包 `@objectstack/connector-imap`。它只做协议和认证:以只读方式连接一个 IMAP
邮箱,读出邮件原文。用 EXAMINE 打开文件夹,用 BODY.PEEK 读取,所以不会把邮件标成已读,也不改动邮箱。
- 支持密码(PLAIN / LOGIN)和 OAuth 令牌(XOAUTH2)。
- 默认 TLS 并校验证书。明文连接只允许连本机。
- 对外部服务器设了内存上限:单行、单个邮件体、单个响应、单条命令的累计响应,超过就拒绝并断开。一个恶意或出错的服务器拖不垮本进程。
- 账号与凭据按"每个用户一个邮箱"的形状设计,共享邮箱是第一个使用者。以后接个人邮箱是扩展,不是重写。
- 无任何运行时依赖。
- `@objectstack/service-mailbox` 新增邮箱同步。
- 部署方组合插件时提供"哪个邮箱用哪个 IMAP
账号和凭据"(`imap.accountFor`),插件把同步注册成定时任务,定期把新邮件交给阶段 1 已上线的接收入口。
- 去重、线程、按邮箱字段自动关联到客户/线索记录、未匹配进池、自动回复与退信过滤,全部沿用阶段 1,没有改动。
- `sys_mailbox` 新增 4 个只读字段,记录同步进度:文件夹、UIDVALIDITY、最后同步的
UID、最后同步时间。凭据不存数据库。
- 顺带完成阶段 1 的两项遗留:
- 转发件为 `message/global`(国际化邮件头)时也能识别发件人;
- README 写明一封邮件最多读 20 个转发件。
- `mailAssociation` 的 liveness 改为 live 这一项,已拆到单独的小 PR #23314 先行落地。
**为什么改**
- 阶段 1 只解决了"拿到一封邮件怎么入库",没有邮件来源,部署上实际收不到邮件。本 PR
补上来源:销售把客户邮件转发或密送到共享邮箱后,邮件会自动挂到对应的客户、线索记录上。这是 CRM 评估者最先会试的功能,属于设计记录
#8998(维护者已「同意」,`5311578770`)的第一阶段。
- 附件提取按席位裁决 B 拆到 #23307,该卡等服务端附件写入能力 #23306 落地后再做。#22871 在本 PR 落地时关闭。
**风险与代价(含回滚)**
- 规模:约 4,170 行变更(含 202 行 LICENSE),超过 3,000 行门槛,需要人工合并。
- 只有显式配置了 `imap.accountFor` 的部署才会连接外部邮箱。仅通过 `requires: ['mailbox']`
自动装配的部署不同步任何邮箱,行为与今天一致,启动时会打印一行说明。
- IMAP 客户端是自研的最小实现,没有引入 imapflow(它带 8 个锁定版本的依赖)。主要风险是与真实邮件服务器的兼容性:
- 已用两个独立的严格 IMAP 测试服务器(imapkit、hoodiecrow)做过一次性互通验证,18 项全部通过;
- 尚未对 Gmail、Microsoft 365 做真机验证。
- 附件暂不保存,转发附件里的原邮件正文也不保留,只读取其中的参与人。
- 单封邮件超过上限(默认 32 MiB;服务器少报或不报大小时同样适用)、或无法解析时,会跳过并记 error
日志,邮件仍留在原邮箱,同步继续往下走,不会卡住。
- 回滚:直接还原本 PR 即可。
- 新增的 4 个字段只是只读的同步进度,还原后残留的列无害。
- 不涉及数据迁移,已入库的邮件保留。
- liveness 那一项在单独的 PR #23314 里,不受影响。
**席位意见**
(终稿与席位意见见 PR 评论 `6157331354`。)
**你要做的**
- 在 PR #23303 上提交一次 Approve 审阅。批准后由席位把它转为 ready 并入队落地;席位在你批准之前不会这样做。
---
_Generated by [Claude
Code](https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6d96721 commit 3d830b7
47 files changed
Lines changed: 4105 additions & 63 deletions
File tree
- .changeset
- content/docs
- permissions
- protocol
- docs/audits
- packages
- connectors/connector-imap
- src
- services/service-mailbox
- src
- ingest
- objects
- sync
- translations
- spec
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
55 | 56 | | |
56 | 57 | | |
57 | 58 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
387 | 387 | | |
388 | 388 | | |
389 | 389 | | |
| 390 | + | |
390 | 391 | | |
391 | 392 | | |
392 | 393 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
| 173 | + | |
173 | 174 | | |
174 | 175 | | |
175 | 176 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
| 125 | + | |
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | | - | |
193 | | - | |
| 192 | + | |
| 193 | + | |
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
| |||
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | | - | |
| 214 | + | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
| 226 | + | |
227 | 227 | | |
228 | | - | |
| 228 | + | |
229 | 229 | | |
230 | 230 | | |
231 | | - | |
| 231 | + | |
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
| 236 | + | |
237 | 237 | | |
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
242 | | - | |
| 242 | + | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | | - | |
| 250 | + | |
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
| |||
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
300 | | - | |
| 300 | + | |
301 | 301 | | |
302 | 302 | | |
303 | 303 | | |
304 | | - | |
305 | | - | |
| 304 | + | |
| 305 | + | |
306 | 306 | | |
307 | | - | |
| 307 | + | |
308 | 308 | | |
309 | 309 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
| 117 | + | |
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| |||
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
195 | | - | |
| 195 | + | |
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
| 36 | + | |
37 | 37 | | |
38 | | - | |
| 38 | + | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
98 | | - | |
| 97 | + | |
| 98 | + | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
208 | 208 | | |
209 | 209 | | |
210 | 210 | | |
| 211 | + | |
211 | 212 | | |
212 | 213 | | |
213 | 214 | | |
| |||
0 commit comments