Mailsy: Add browser inbox bridge page#26950
Conversation
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.
|
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 commandsBRANCH="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 devWe're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 10-15 business days. |
There was a problem hiding this comment.
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()inapi.tsembedsaccount.tokenin a plaintext JavaScript variable inside the generated HTML file, persisting the credential on disk- The token-containing file is never cleaned up in
deleteAccount()orremoveAccount()(logout), leaving the credential accessible even after sign-out - The
onActionasync handler inMail.tsxdoes not catch errors fromwriteBridgePage, leaving the user with no feedback if the file write fails CHANGELOG.mdwas not updated with a new entry for this PR (required by project convention)sandbox="allow-same-origin"on the messageiframegrantsfile://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.tsrequires significant changes to handle token security;extensions/mailsy/src/components/Mail.tsxneeds 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} |
There was a problem hiding this comment.
[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:
- Token survives logout/account deletion. When the user calls
removeAccount()(logout) ordeleteAccount(), their LocalStorage data is cleared — butinbox.htmlis never deleted. The credential continues to live on disk even after the user believes they have fully signed out. - 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)||'')+' <'+esc((m.from&&m.from.address)||'')+'> \\u00b7 '+new Date(m.createdAt).toLocaleString()+'</div>' | ||
| +(h?'<iframe sandbox="allow-same-origin" srcdoc="'+h.replace(/&/g,'&').replace(/"/g,'"')+'"></iframe>':'<p>'+esc(m.text||'No content')+'</p>') | ||
| +'</div>'; |
There was a problem hiding this comment.
[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`, | ||
| ) |
There was a problem hiding this comment.
[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 SummaryThis PR replaces the plain "Open in Browser" action (which previously just linked to
Confidence Score: 1/5Not 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
Important Files Changed
|
| </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}; |
There was a problem hiding this 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:
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.htmlas 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.| onAction={async () => { | ||
| if (!account) return; | ||
| const filePath = await writeBridgePage(account); | ||
| await open(`file://${filePath}`); | ||
| }} |
There was a problem hiding this 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:
| 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.| +'<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)||'')+' <'+esc((m.from&&m.from.address)||'')+'> \\u00b7 '+new Date(m.createdAt).toLocaleString()+'</div>' | ||
| +(h?'<iframe sandbox="allow-same-origin" srcdoc="'+h.replace(/&/g,'&').replace(/"/g,'"')+'"></iframe>':'<p>'+esc(m.text||'No content')+'</p>') |
There was a problem hiding this 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:
| +(h?'<iframe sandbox="allow-same-origin" srcdoc="'+h.replace(/&/g,'&').replace(/"/g,'"')+'"></iframe>':'<p>'+esc(m.text||'No content')+'</p>') | |
| +(h?'<iframe sandbox srcdoc="'+h.replace(/&/g,'&').replace(/"/g,'"')+'></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,'&').replace(/"/g,'"')+'></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
|
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 😊 |
Summary
getBridgePagePath()andwriteBridgePage()functions to generate a self-contained HTML page with inline CSS/JS that fetches messages directly via the mail.tm APIChanges
src/components/Mail.tsx— Updated "Open in Browser" action to generate and open the bridge page instead of linking to mail.tmsrc/libs/api.ts— AddedgetBridgePagePath()andwriteBridgePage()functions (~84 lines)