Skip to content

Mailsy: Add browser inbox bridge page#26950

Open
jakkpotts wants to merge 3 commits intoraycast:mainfrom
jakkpotts:mailsy/add-browser-inbox-bridge
Open

Mailsy: Add browser inbox bridge page#26950
jakkpotts wants to merge 3 commits intoraycast:mainfrom
jakkpotts:mailsy/add-browser-inbox-bridge

Conversation

@jakkpotts
Copy link
Copy Markdown

Summary

  • Replaces the "Open in Browser" action on the account item with a local HTML bridge page that provides a full browser-based inbox experience using the mail.tm API
  • Adds getBridgePagePath() and writeBridgePage() functions to generate a self-contained HTML page with inline CSS/JS that fetches messages directly via the mail.tm API
  • Users can now view their inbox, read individual messages, and navigate between messages entirely in the browser

Changes

  • src/components/Mail.tsx — Updated "Open in Browser" action to generate and open the bridge page instead of linking to mail.tm
  • src/libs/api.ts — Added getBridgePagePath() and writeBridgePage() functions (~84 lines)

Replace the "Open in Browser" action on the account item to generate a
local HTML bridge page that provides a full browser-based inbox experience
using the mail.tm API, instead of simply linking to mail.tm.
@raycastbot raycastbot added extension fix / improvement Label for PRs with extension's fix improvements extension: mailsy Issues related to the mailsy extension platform: macOS platform: Windows labels Apr 5, 2026
@raycastbot
Copy link
Copy Markdown
Collaborator

raycastbot commented Apr 5, 2026

Thank you for your first contribution! 🎉

🔔 @BalliAsghar you might want to have a look.

You can use this guide to learn how to check out the Pull Request locally in order to test it.

📋 Quick checkout commands
BRANCH="mailsy/add-browser-inbox-bridge"
FORK_URL="https://github.com/jakkpotts/extensions.git"
EXTENSION_NAME="mailsy"
REPO_NAME="extensions"

git clone -n --depth=1 --filter=tree:0 -b $BRANCH $FORK_URL
cd $REPO_NAME
git sparse-checkout set --no-cone "extensions/$EXTENSION_NAME"
git checkout
cd "extensions/$EXTENSION_NAME"
npm install && npm run dev

We're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 10-15 business days.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Review Summary

This PR replaces the "Open in Browser" action with a locally-generated HTML bridge page that renders the inbox and messages using the mail.tm API. While the idea is creative, it introduces a critical security issue: the bearer token is written in plaintext to a static file on disk (inbox.html) that is never deleted when the user logs out or deletes their account. Additionally, the onAction handler for the bridge page lacks error handling, and the CHANGELOG was not updated.

Key issues:

  • writeBridgePage() in api.ts embeds account.token in a plaintext JavaScript variable inside the generated HTML file, persisting the credential on disk
  • The token-containing file is never cleaned up in deleteAccount() or removeAccount() (logout), leaving the credential accessible even after sign-out
  • The onAction async handler in Mail.tsx does not catch errors from writeBridgePage, leaving the user with no feedback if the file write fails
  • CHANGELOG.md was not updated with a new entry for this PR (required by project convention)
  • sandbox="allow-same-origin" on the message iframe grants file:// origin to srcdoc content; dropping it would provide stricter null-origin isolation

File Analysis

File Confidence Notes
extensions/mailsy/src/libs/api.ts 1 Adds writeBridgePage() that embeds the bearer token in plaintext in a local HTML file that is never cleaned up on logout/account-deletion
extensions/mailsy/src/components/Mail.tsx 3 Replaces Action.OpenInBrowser with a custom Action that generates and opens a local bridge page; missing error handling

Overall confidence score: 1 — Not safe to merge — bearer token is written to a persistent plaintext file on disk and never cleaned up on logout or account deletion.

Score of 1 reflects a critical security issue: the auth token is persisted to disk in plaintext and survives logout/account deletion, plus missing error handling in the action handler and no CHANGELOG update.

Files needing attention: extensions/mailsy/src/libs/api.ts requires significant changes to handle token security; extensions/mailsy/src/components/Mail.tsx needs error handling.


CHANGELOG.md not updated (extensions/mailsy/CHANGELOG.md)

The repository requires a new entry at the top of the CHANGELOG for every PR, using the {PR_MERGE_DATE} placeholder. The latest entry is dated 2026-03-19 and no new entry has been added for this PR. Please add an entry such as:

## [Changes] - {PR_MERGE_DATE}

- Added browser inbox bridge page for the account action.

<div class="container"><div id="c"><div class="loading">Loading\u2026</div></div></div>
<script>
var T='${account.token}',A='${BASE_URL}',H={Authorization:'Bearer '+T};
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P0 - Security] Bearer token written to plaintext HTML file on disk and never cleaned up

This line interpolates account.token directly into the generated HTML as a JavaScript variable:

var T='${account.token}', ...

This bearer token grants full API access to the mail account and is now persisted to disk at a predictable, static path (environment.assetsPath/inbox.html). There are two serious consequences:

  1. Token survives logout/account deletion. When the user calls removeAccount() (logout) or deleteAccount(), their LocalStorage data is cleared — but inbox.html is never deleted. The credential continues to live on disk even after the user believes they have fully signed out.
  2. Accessible to other local processes. Because the file is written to environment.assetsPath, any local process with read access to that directory can trivially extract the token.

Recommended fix: Never embed credentials in files on disk. Instead, load the token from Raycast's secure storage at the moment it is needed (e.g., via a Raycast command or a dedicated local server that proxies the API calls), and ensure that any generated HTML file is deleted when the user logs out or deletes their account.

+'<h2>'+esc(m.subject||'No Subject')+'</h2>'
+'<div class="meta">From: '+esc((m.from&&m.from.name)||'')+' &lt;'+esc((m.from&&m.from.address)||'')+'&gt; \\u00b7 '+new Date(m.createdAt).toLocaleString()+'</div>'
+(h?'<iframe sandbox="allow-same-origin" srcdoc="'+h.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'"></iframe>':'<p>'+esc(m.text||'No content')+'</p>')
+'</div>';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2 - Style] sandbox="allow-same-origin" weakens sandboxing for local file content

When rendering srcdoc content inside a page served from file:// origin, adding allow-same-origin to the sandbox gives the iframe content the same file:// origin as the parent page, rather than the stricter null origin you get without it.

Because allow-scripts is not set, active scripts cannot run inside this iframe, which limits the practical impact. However, removing allow-same-origin would provide a stronger default sandbox and is the safer baseline — especially since this page itself already embeds a live bearer token.

Suggested change:

<!-- before -->
<iframe sandbox="allow-same-origin" srcdoc="...">
<!-- after -->
<iframe sandbox="" srcdoc="...">
<!-- or simply omit the sandbox attribute value: -->
<iframe sandbox srcdoc="...">

`Logging out...`,
`Logout successful`,
`Logout failed`,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1 - Style] No error handling in the onAction handler

This async callback has no try/catch. If writeBridgePage() throws (e.g., due to a filesystem write error), the rejection is silently swallowed and the user receives no feedback.

The rest of the extension uses handleAction() from utils for exactly this purpose — wrapping async operations and surfacing errors via a toast or HUD. Please apply the same pattern here:

onAction={() =>
  handleAction(async () => {
    if (!account) return;
    const filePath = await writeBridgePage(account);
    await open(`file://${filePath}`);
  })
}

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 5, 2026

Greptile Summary

This PR replaces the plain "Open in Browser" action (which previously just linked to mail.tm) with a locally-generated HTML bridge page that authenticates against the mail.tm API and renders the inbox and individual messages inside the browser. While the idea is creative, it introduces a critical security issue: the bearer token is written verbatim into a JavaScript variable inside a static HTML file on disk, and that file is never deleted when the user logs out or deletes their account. Additionally, the new onAction handler is missing error handling, and CHANGELOG.md was not updated.

  • writeBridgePage() in api.ts embeds account.token as a plaintext JavaScript string literal (var T='<token>') in the generated inbox.html file
  • The token-containing file is never cleaned up in deleteAccount() or removeAccount() (logout), meaning the credential persists on disk even after the user believes they have fully signed out
  • The onAction async handler in Mail.tsx has no try/catch; a filesystem write failure will be silently swallowed with no toast or HUD feedback — the rest of the extension uses handleAction() for exactly this purpose
  • CHANGELOG.md was not updated with a new entry for this PR (required by project convention)
  • sandbox="allow-same-origin" on the message <iframe srcdoc> grants the email's HTML content the same file:// origin as the parent page rather than the stricter null origin you get by omitting allow-same-origin; since allow-scripts is absent this is low-severity, but dropping allow-same-origin would provide a stronger sandbox

Confidence Score: 1/5

Not safe to merge — the bearer token is written to a persistent plaintext file on disk and is never cleaned up on logout or account deletion

Score of 1 reflects a critical security issue: the auth token is persisted to disk in cleartext inside a file that survives logout/account-deletion, plus a missing error handler in the action callback and a missing CHANGELOG update

extensions/mailsy/src/libs/api.ts requires significant changes to address token persistence; extensions/mailsy/src/components/Mail.tsx needs error handling added to the onAction callback

Important Files Changed

Filename Overview
extensions/mailsy/src/libs/api.ts Adds writeBridgePage() which embeds the bearer token in a plaintext HTML file on disk that is never deleted on logout or account deletion — critical security issue
extensions/mailsy/src/components/Mail.tsx Replaces Action.OpenInBrowser with a custom Action that generates and opens the local bridge page; the onAction handler is missing error handling

Comments Outside Diff (1)

  1. extensions/mailsy/CHANGELOG.md, line 1 (link)

    P1 CHANGELOG not updated for this PR

    The project requires a new entry to be added at the top of CHANGELOG.md for every PR, using the {PR_MERGE_DATE} placeholder. Please add an entry such as:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/mailsy/CHANGELOG.md
    Line: 1
    
    Comment:
    **CHANGELOG not updated for this PR**
    
    The project requires a new entry to be added at the top of `CHANGELOG.md` for every PR, using the `{PR_MERGE_DATE}` placeholder. Please add an entry such as:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/mailsy/src/libs/api.ts
Line: 151

Comment:
**Bearer token written to plaintext file on disk — never cleaned up on logout/account deletion**

The auth token is embedded directly into a JavaScript string literal in the generated HTML file:

```js
var T='${account.token}', ...
```

This means the full bearer token sits in `inbox.html` on disk in cleartext. More critically, neither `deleteAccount()` nor the logout (`removeAccount()`) function deletes this file, so the credential survives after the user signs out or deletes their account.

Consider one or more of the following mitigations:
- Delete `inbox.html` as part of account deletion / logout cleanup.
- Avoid embedding the token in the file at all — instead have the page read it from a secure, ephemeral source (e.g., a query param that is consumed once, or a short-lived one-time token).
- At minimum, document that the file must be manually removed and clear it on extension unload.

Without this fix, a second OS user (or any process with access to the Raycast assets directory) can extract the token and read the victim's email.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/mailsy/src/components/Mail.tsx
Line: 127-131

Comment:
**Missing error handling in `onAction` — write failures are silently swallowed**

The async handler calls `writeBridgePage()` and `open()` but has no `try/catch`. If the filesystem write fails (e.g., permissions, disk full) the user will see nothing — no toast, no HUD.

Every other destructive action in this file uses `handleAction()` for exactly this reason. Consider wrapping similarly:

```suggestion
                onAction={async () => {
                  if (!account) return;
                  try {
                    const filePath = await writeBridgePage(account);
                    await open(`file://${filePath}`);
                  } catch (e) {
                    await showHUD("Failed to open inbox in browser");
                  }
                }}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/mailsy/src/libs/api.ts
Line: 177

Comment:
**`sandbox="allow-same-origin"` weakens iframe isolation for `file://`-origin content**

When a `<iframe srcdoc>` is loaded inside a `file://` page, omitting `allow-same-origin` gives the srcdoc content a null (opaque) origin — the strongest sandbox. Adding `allow-same-origin` promotes it to the parent's `file://` origin. Since `allow-scripts` is absent no JS executes, keeping the practical impact low; but removing `allow-same-origin` is the stricter and more correct choice for rendering untrusted email HTML:

```suggestion
+(h?'<iframe sandbox srcdoc="'+h.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'></iframe>':'<p>'+esc(m.text||'No content')+'</p>')
```

(`sandbox` with no value gives the most restrictive sandbox.)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/mailsy/CHANGELOG.md
Line: 1

Comment:
**CHANGELOG not updated for this PR**

The project requires a new entry to be added at the top of `CHANGELOG.md` for every PR, using the `{PR_MERGE_DATE}` placeholder. Please add an entry such as:

```suggestion
## [Add browser inbox bridge page] - {PR_MERGE_DATE}

- Replaced "Open in Browser" link with a self-contained HTML bridge page that fetches and displays the inbox and messages using the mail.tm API.

# Mailsy Changelog
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Add browser inbox bridge page for mailsy..." | Re-trigger Greptile

</div>
<div class="container"><div id="c"><div class="loading">Loading\u2026</div></div></div>
<script>
var T='${account.token}',A='${BASE_URL}',H={Authorization:'Bearer '+T};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 Bearer token written to plaintext file on disk — never cleaned up on logout/account deletion

The auth token is embedded directly into a JavaScript string literal in the generated HTML file:

var T='${account.token}', ...

This means the full bearer token sits in inbox.html on disk in cleartext. More critically, neither deleteAccount() nor the logout (removeAccount()) function deletes this file, so the credential survives after the user signs out or deletes their account.

Consider one or more of the following mitigations:

  • Delete inbox.html as part of account deletion / logout cleanup.
  • Avoid embedding the token in the file at all — instead have the page read it from a secure, ephemeral source (e.g., a query param that is consumed once, or a short-lived one-time token).
  • At minimum, document that the file must be manually removed and clear it on extension unload.

Without this fix, a second OS user (or any process with access to the Raycast assets directory) can extract the token and read the victim's email.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/mailsy/src/libs/api.ts
Line: 151

Comment:
**Bearer token written to plaintext file on disk — never cleaned up on logout/account deletion**

The auth token is embedded directly into a JavaScript string literal in the generated HTML file:

```js
var T='${account.token}', ...
```

This means the full bearer token sits in `inbox.html` on disk in cleartext. More critically, neither `deleteAccount()` nor the logout (`removeAccount()`) function deletes this file, so the credential survives after the user signs out or deletes their account.

Consider one or more of the following mitigations:
- Delete `inbox.html` as part of account deletion / logout cleanup.
- Avoid embedding the token in the file at all — instead have the page read it from a secure, ephemeral source (e.g., a query param that is consumed once, or a short-lived one-time token).
- At minimum, document that the file must be manually removed and clear it on extension unload.

Without this fix, a second OS user (or any process with access to the Raycast assets directory) can extract the token and read the victim's email.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +127 to +131
onAction={async () => {
if (!account) return;
const filePath = await writeBridgePage(account);
await open(`file://${filePath}`);
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing error handling in onAction — write failures are silently swallowed

The async handler calls writeBridgePage() and open() but has no try/catch. If the filesystem write fails (e.g., permissions, disk full) the user will see nothing — no toast, no HUD.

Every other destructive action in this file uses handleAction() for exactly this reason. Consider wrapping similarly:

Suggested change
onAction={async () => {
if (!account) return;
const filePath = await writeBridgePage(account);
await open(`file://${filePath}`);
}}
onAction={async () => {
if (!account) return;
try {
const filePath = await writeBridgePage(account);
await open(`file://${filePath}`);
} catch (e) {
await showHUD("Failed to open inbox in browser");
}
}}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/mailsy/src/components/Mail.tsx
Line: 127-131

Comment:
**Missing error handling in `onAction` — write failures are silently swallowed**

The async handler calls `writeBridgePage()` and `open()` but has no `try/catch`. If the filesystem write fails (e.g., permissions, disk full) the user will see nothing — no toast, no HUD.

Every other destructive action in this file uses `handleAction()` for exactly this reason. Consider wrapping similarly:

```suggestion
                onAction={async () => {
                  if (!account) return;
                  try {
                    const filePath = await writeBridgePage(account);
                    await open(`file://${filePath}`);
                  } catch (e) {
                    await showHUD("Failed to open inbox in browser");
                  }
                }}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread extensions/mailsy/src/libs/api.ts Outdated
+'<a class="back" onclick="loadInbox()">\\u2190 Back to Inbox</a>'
+'<h2>'+esc(m.subject||'No Subject')+'</h2>'
+'<div class="meta">From: '+esc((m.from&&m.from.name)||'')+' &lt;'+esc((m.from&&m.from.address)||'')+'&gt; \\u00b7 '+new Date(m.createdAt).toLocaleString()+'</div>'
+(h?'<iframe sandbox="allow-same-origin" srcdoc="'+h.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'"></iframe>':'<p>'+esc(m.text||'No content')+'</p>')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 sandbox="allow-same-origin" weakens iframe isolation for file://-origin content

When a <iframe srcdoc> is loaded inside a file:// page, omitting allow-same-origin gives the srcdoc content a null (opaque) origin — the strongest sandbox. Adding allow-same-origin promotes it to the parent's file:// origin. Since allow-scripts is absent no JS executes, keeping the practical impact low; but removing allow-same-origin is the stricter and more correct choice for rendering untrusted email HTML:

Suggested change
+(h?'<iframe sandbox="allow-same-origin" srcdoc="'+h.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'"></iframe>':'<p>'+esc(m.text||'No content')+'</p>')
+(h?'<iframe sandbox srcdoc="'+h.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'></iframe>':'<p>'+esc(m.text||'No content')+'</p>')

(sandbox with no value gives the most restrictive sandbox.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/mailsy/src/libs/api.ts
Line: 177

Comment:
**`sandbox="allow-same-origin"` weakens iframe isolation for `file://`-origin content**

When a `<iframe srcdoc>` is loaded inside a `file://` page, omitting `allow-same-origin` gives the srcdoc content a null (opaque) origin — the strongest sandbox. Adding `allow-same-origin` promotes it to the parent's `file://` origin. Since `allow-scripts` is absent no JS executes, keeping the practical impact low; but removing `allow-same-origin` is the stricter and more correct choice for rendering untrusted email HTML:

```suggestion
+(h?'<iframe sandbox srcdoc="'+h.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'></iframe>':'<p>'+esc(m.text||'No content')+'</p>')
```

(`sandbox` with no value gives the most restrictive sandbox.)

How can I resolve this? If you propose a fix, please make it concise.

- Delete bridge page on logout and account deletion to prevent
  token persistence on disk
- Wrap Open in Browser action with handleAction for error feedback
- Remove allow-same-origin from iframe sandbox for stricter isolation
- Add CHANGELOG entry
@raycastbot
Copy link
Copy Markdown
Collaborator

This pull request has been automatically marked as stale because it did not have any recent activity.

It will be closed if no further activity occurs in the next 7 days to keep our backlog clean 😊

@raycastbot raycastbot added the status: stalled Stalled due inactivity label Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extension fix / improvement Label for PRs with extension's fix improvements extension: mailsy Issues related to the mailsy extension platform: macOS platform: Windows status: stalled Stalled due inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants