Skip to content

Conversation

@EvanHahn
Copy link
Contributor

@EvanHahn EvanHahn commented Jan 19, 2026

ref https://linear.app/ghost/issue/FEA-483/sniperlink-support-in-portal-converted-to-project

This adds the @tryghost/parse-email-address package, which extracts the local and domain parts of email address strings. For example:

parseEmailAddress('[email protected]');
// => {local: 'foo', domain: 'example.com'}

We'll be able to use this in various places across Ghost. I'll start by parsing the domain for "sniper links" (see Linear).


Note

Adds a new reusable package for extracting and normalizing email address parts.

  • New @tryghost/parse-email-address with parseEmailAddress(email) returning {local, domain} or null; wraps parse-email-address and normalizes domains via node:url domainToASCII
  • Includes tests covering invalid cases, quoted locals, case-insensitivity, and internationalized domains
  • Adds package scaffolding (README.md, LICENSE, tsconfig.json, ESLint configs) and updates yarn.lock

Written by Cursor Bugbot for commit 3b90b96. This will update automatically on new commits. Configure here.

ref https://linear.app/ghost/issue/FEA-483/sniperlink-support-in-portal-converted-to-project

This adds the `@tryghost/parse-email-address` package, which extracts
the local and domain parts of email address strings. For example:

```
parseEmailAddress('[email protected]');
// => {local: 'foo', domain: 'example.com'}
```

We'll be able to use this in various places across Ghost. I'll start by
parsing the domain for "sniper links" (see Linear).
@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Walkthrough

This PR introduces a new npm package @tryghost/parse-email-address to the monorepo. It includes package configuration (package.json), TypeScript compilation setup (tsconfig.json), ESLint configurations for the package and tests, a primary module that exports a parseEmailAddress function wrapping upstream email parsing logic with domain ASCII conversion, a comprehensive test suite validating email parsing for both valid and invalid inputs, documentation (README.md), and an MIT License file.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a new parse-email-address package to the monorepo.
Description check ✅ Passed The description is directly related to the changeset, providing context, usage examples, and details about the new package functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.18.1)
packages/parse-email-address/README.md

markdownlint-cli2 v0.18.1 (markdownlint v0.38.0)
Finding: packages/parse-email-address/README.md
Linting: 1 file(s)
Summary: 3 error(s)
Error: EACCES: permission denied, open '/markdownlint-cli2-results.json'
at async open (node:internal/fs/promises:640:25)
at async Object.writeFile (node:internal/fs/promises:1214:14)
at async Promise.all (index 0)
at async outputSummary (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:877:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:1053:25)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:12:22 {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: '/markdownlint-cli2-results.json'
}


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */

/* Type Checking */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I enabled much more strictness than Slimer's default. This should not affect anything outside this package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made no manual changes to this file other than (1) what Slimer generated (2) installing a dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made no changes to either ESLint file.

@EvanHahn EvanHahn marked this pull request as ready for review January 19, 2026 19:16
@EvanHahn EvanHahn requested a review from 9larsons January 19, 2026 19:16
@codecov-commenter
Copy link

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.21%. Comparing base (4eae5e6) to head (3b90b96).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #340      +/-   ##
==========================================
+ Coverage   90.18%   90.21%   +0.02%     
==========================================
  Files         112      113       +1     
  Lines        7621     7641      +20     
  Branches     1092     1096       +4     
==========================================
+ Hits         6873     6893      +20     
  Misses        740      740              
  Partials        8        8              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/parse-email-address/README.md`:
- Around line 28-29: Update the README text to hyphenate “top-level domain”
wherever it appears; specifically replace the phrase "The top level domain must
have at least two octets." with "The top-level domain must have at least two
octets." (and similarly update any other occurrences of "top level domain") so
the documentation uses correct grammar.

Comment on lines +28 to +29
- Domain names must have at least two labels. `example.com` is okay, `example` is not.
- The top level domain must have at least two octets. `example.com` is okay, `example.x` is not.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hyphenate “top-level domain.”

Minor doc grammar: “top level domain” should be “top-level domain”.

✏️ Proposed doc tweak
-- The top level domain must have at least two octets. `example.com` is okay, `example.x` is not.
+- The top-level domain must have at least two octets. `example.com` is okay, `example.x` is not.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Domain names must have at least two labels. `example.com` is okay, `example` is not.
- The top level domain must have at least two octets. `example.com` is okay, `example.x` is not.
- Domain names must have at least two labels. `example.com` is okay, `example` is not.
- The top-level domain must have at least two octets. `example.com` is okay, `example.x` is not.
🧰 Tools
🪛 LanguageTool

[grammar] ~29-~29: Use a hyphen to join words.
Context: ...omis okay,example` is not. - The top level domain must have at least two octe...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In `@packages/parse-email-address/README.md` around lines 28 - 29, Update the
README text to hyphenate “top-level domain” wherever it appears; specifically
replace the phrase "The top level domain must have at least two octets." with
"The top-level domain must have at least two octets." (and similarly update any
other occurrences of "top level domain") so the documentation uses correct
grammar.

Copy link
Contributor

@9larsons 9larsons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Where'd you find this package? I think that's my only real question/concern, though it's recent and shouldn't really change 🤷.

@EvanHahn
Copy link
Contributor Author

Found it when searching "RFC 5321" on npm. I reviewed the code and it looks almost exactly like what we need.

@EvanHahn EvanHahn merged commit a5384b4 into main Jan 19, 2026
4 checks passed
@EvanHahn EvanHahn deleted the parse-email-address-package branch January 19, 2026 22:37
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 20, 2026
closes https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

  Before: if `req.body.email` wasn't a string or lacked an `@`, no
  normalization would occur. The system would then attempt to send an
  email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 21, 2026
closes https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core
ref #25919
ref TryGhost/framework#342

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

  Before: if `req.body.email` wasn't a string or lacked an `@`, no
  normalization would occur. The system would then attempt to send an
  email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Long emails are now considered invalid.

  Before: the only limit on email length was that of the request body.

  After: the whole email is limited to 986 octets and the domain is
  limited to 253 octets (per SMTP). Domain labels are limited to 63
  octets (per DNS).

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 21, 2026
closes https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core
ref #25919
ref TryGhost/framework#342

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

  Before: if `req.body.email` wasn't a string or lacked an `@`, no
  normalization would occur. The system would then attempt to send an
  email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Long emails are now considered invalid.

  Before: the only limit on email length was that of the request body.

  After: the whole email is limited to 986 octets and the domain is
  limited to 253 octets (per SMTP). Domain labels are limited to 63
  octets (per DNS).

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 21, 2026
closes https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core
ref #25919
ref TryGhost/framework#342

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

  Before: if `req.body.email` wasn't a string or lacked an `@`, no
  normalization would occur. The system would then attempt to send an
  email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Long emails are now considered invalid.

  Before: the only limit on email length was that of the request body.

  After: the whole email is limited to 986 octets and the domain is
  limited to 253 octets (per SMTP). Domain labels are limited to 63
  octets (per DNS).

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 22, 2026
closes https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core
ref #25919
ref TryGhost/framework#342

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

  Before: if `req.body.email` wasn't a string or lacked an `@`, no
  normalization would occur. The system would then attempt to send an
  email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Long emails are now considered invalid.

  Before: the only limit on email length was that of the request body.

  After: the whole email is limited to 986 octets and the domain is
  limited to 253 octets (per SMTP). Domain labels are limited to 63
  octets (per DNS).

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 26, 2026
closes https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core
ref #25919
ref TryGhost/framework#342

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

  Before: if `req.body.email` wasn't a string or lacked an `@`, no
  normalization would occur. The system would then attempt to send an
  email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Long emails are now considered invalid.

  Before: the only limit on email length was that of the request body.

  After: the whole email is limited to 986 octets and the domain is
  limited to 253 octets (per SMTP). Domain labels are limited to 63
  octets (per DNS).

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340
EvanHahn added a commit to TryGhost/Ghost that referenced this pull request Jan 26, 2026
closes
https://linear.app/ghost/issue/NY-942/create-and-use-parse-email-address-package-in-core
ref #25919
ref TryGhost/framework#342

This creates the `parse-email-address` package (previously reviewed
[here][0]--all I did was move it over) and uses it in email
normalization, used when sending magic links.

This should not change behavior for "normal", lowercase ASCII email
addresses. However, it does make several subtler tweaks to the way
emails are normalized:

- Some totally invalid emails weren't normalized.

Before: if `req.body.email` wasn't a string or lacked an `@`, no
normalization would occur. The system would then attempt to send an
email that'd almost certainly fail.

  After: these throw a "bad request" error.

- Invalid emails with multiple `@` signs were considered valid.

  Before: `foo@[email protected]` was accepted.

  After: it is not accepted and throws a "bad request" error.

- Domains are now lowercased.

  Before: `[email protected]` was normalized to `[email protected]`.

  After: `[email protected]` is normalized to `[email protected]`.

[0]: TryGhost/framework#340

Co-authored-by: Steve Larson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants