Status: Draft v0.4 Owner: Michael A. Alderete, Aldosoft Date: 2026-05-18
A single-page web tool that converts an email address (or an arbitrary HTML
snippet containing an email address) into an obfuscated JavaScript snippet
the user can paste into their own web page. When the snippet runs in a
visitor's browser, the JavaScript decodes itself and writes a normal
mailto: link into the DOM. Email-harvesting bots that parse HTML but do
not execute JavaScript see only opaque code.
The tool is a modern recreation of the spirit of the Hivelogic Enkoder web
form (Dan Benjamin, 2006–2010s). It does not carry Hivelogic branding
or chrome, does not need to produce byte-identical output to the
original, and is delivered as a single self-contained .html file with
inline CSS and JavaScript.
- Give a non-technical user a one-screen way to turn an email address into paste-ready obfuscated HTML.
- Give a technical user a way to obfuscate an arbitrary HTML snippet (so the email address is not the only thing that can be hidden).
- Produce output that survives copy/paste into any CMS, static site, or hand-written HTML page, with no runtime dependencies.
- Be readable, accessible, mobile-friendly, and visually modern.
- Be trivially auditable: the entire application is one file you can open,
read, and serve from
file://, a CDN, or any static host.
- Server-side rendering, backend APIs, accounts, or persistence beyond the user's own browser.
- A WordPress plugin, Rails helper, or PHP library (the prior art).
- Bulk encoding, address harvesting protection on arbitrary pages, or scanning third-party content.
- Guaranteed defeat of sophisticated headless-browser harvesters. The output raises the cost of harvesting; it does not eliminate it. This caveat is shown to the user in-product.
- Telemetry, analytics, ads, or any outbound network call. The page must function fully offline once loaded.
- Operating as a hosted or multi-tenant service. Obfuskoder is a tool you run yourself; its security properties are stated against the single-user, single-browser threat model, not against arbitrary visitors submitting untrusted input through a hosted endpoint.
Primary persona: a site owner who wants to publish a contact email without exposing it as plaintext in their HTML.
| # | As a… | I want to… | So that… |
|---|---|---|---|
| US-1 | site owner | type my email, link text, optional title and subject | I get a ready-to-paste obfuscated mailto: link |
| US-2 | developer | paste arbitrary HTML containing an email | I can obfuscate richer markup (image, span, etc.) |
| US-3 | any user | preview what visitors will see | I trust the output before publishing it |
| US-4 | any user | one-click copy the encoded output | I do not have to select-all-and-copy from a textarea |
| US-5 | keyboard user | tab through the form, submit, and copy without a mouse | the tool is usable with assistive tech |
| US-6 | mobile user | use the tool on a phone | I do not need a desktop |
The page consists of three vertically stacked regions:
- Header. Product name Obfuskoder, one-sentence tagline, one-paragraph explanation of what the tool does and why. No external logos, no "Hivelogic" reference in the running UI. A short caveat: "This raises the cost of email harvesting. It does not guarantee zero spam."
- Tabbed form area. Two tabs (or two side-by-side cards on wide screens): Basic and Advanced. Only one is active at a time.
- Result area. Hidden until the user clicks the Obfuskode button. Reveals: (a) a read-only textarea with the encoded HTML+JS snippet, (b) a Copy button, (c) a Preview panel rendering the snippet in a sandboxed iframe.
Fields (all <input type="text"> unless noted):
| Field | Label | Required | Validation |
|---|---|---|---|
email |
Email address | yes | matches /^[^\s@]+@[^\s@]+\.[^\s@]+$/; trimmed |
linkText |
Link text | yes | non-empty after trim; defaults visually to the email if blank when user tabs out (UX nicety, not enforced) |
linkTitle |
Link title (tooltip) | no | any string; trimmed |
subject |
Subject (optional) | no | any string; URL-encoded into the mailto: href when present |
Submit button label: Obfuskode.
On submit:
- Validate the email pattern. If invalid, show an inline error under the
field, set
aria-invalid="true", do not encode. - Build the canonical
<a href="mailto:EMAIL?subject=SUBJ" title="TITLE">TEXT</a>string. Omit?subject=and thetitleattribute when empty. - Pass that HTML string to the encoder (§6).
- Render result + preview.
A single <textarea> labeled HTML to obfuskode, prefilled with a small
placeholder example like <a href="mailto:user@example.com">Email me</a>.
The field hint notes that surrounding whitespace is trimmed.
Submit button label: Obfuskode.
On submit:
- Trim. If empty, show an inline error.
- Pass the textarea content verbatim to the encoder.
- Render result + preview.
No HTML sanitization is performed — the user is explicitly opting into "paste whatever you want." The preview iframe is sandboxed (§5.5) so this does not put the tool itself at risk.
- Read-only
<textarea>showing the full snippet (HTML +<script>+<noscript>/fallback<span>). Pre-selected on focus. - Copy button using the async Clipboard API (
navigator.clipboard .writeText). On success, transient inline confirmation ("Copied" visible for ~2s and announced viaaria-live="polite"). On failure, fall back to selecting the textarea and instructing the user to press ⌘/Ctrl-C. - Preview panel:
<iframe sandbox="allow-scripts">whosesrcdocis set to a minimal HTML document that includes the encoded snippet. The iframe runs the JS in isolation from the host page, so any badness in user-supplied HTML cannot touch the encoder UI. - Below the preview: a "Show decoded HTML source" disclosure that reveals the decoded HTML (equal to the user's input by ENC-1) so the user can verify the round-trip.
The snippet the user receives MUST:
- Be valid standalone HTML that can be pasted anywhere in
<body>. - Contain no readable instance of the original email address (no substring of the address present as text, in any reasonable static scan — see §6.3 for the property test).
- Include a
<noscript>-equivalent fallback. Preferred form: a<span>with a fallback message that the decoder replaces. Default message: "Enable JavaScript to view email". The message is held in the string table near the top of the script (§7.6) so a future version can expose it as a user input.- Implementation note: any approach achieving the no-
@, no-plaintext properties is acceptable. The standalone-enkodergetElementById('ENKODER_ID').outerHTMLpattern is a workable reference. - Naming note: if you copy that pattern, rename the sentinel
constant (e.g.,
OBFUSKODER_ID) so the legacy "Enkoder" name does not appear in the shipped source.
- Implementation note: any approach achieving the no-
- Be self-contained: no
src=, no external resource, no module import. - Work in modern evergreen browsers (§7.3) and in pages loaded via AJAX
(i.e., must not depend on
document.writepost-parse).
The snippet SHOULD:
- Be visually unique per encode (random seed) so two encodes of the same input produce different snippets, raising the cost of pattern-matching against the encoder itself.
- Fit comfortably within reasonable size bounds (default target: under ~3 KB for a short email address; hard cap not required).
- Both forms are present in the DOM; tabs toggle visibility, not mount.
- Submitting either form triggers the encoder synchronously (no network).
- Re-encoding the same inputs produces a fresh snippet each time (different randomness).
- Inputs are not persisted across page reloads. (Out of scope for v1; see §10.)
This spec defines properties the encoder must satisfy, not the exact algorithm. v1 picks an implementation that meets these properties; later versions may swap it freely.
| ID | Property |
|---|---|
| ENC-1 | Round-trip correctness. For any input HTML string s, executing the produced snippet in a JS environment yields the exact same s injected into the DOM at the snippet's location. |
| ENC-2 | No reconstructable leakage. The static text of the snippet contains no occurrence of s as a substring, no occurrence of the raw email address, and no occurrence of the local-part adjacent to the domain (or to an @-like delimiter). Incidental substring matches in unrelated context — e.g., a local-part email coinciding with the word "email" in the fallback message — are not leaks: they yield no path for a harvester to reconstruct the address. |
| ENC-3 | No @ in static text. The character @ does not appear in the static snippet. (Heuristic harvesters key off @.) |
| ENC-4 | Self-contained. Snippet pulls nothing over the network. |
| ENC-5 | Deterministic decode. The decoder runs in bounded time (no infinite loops, no setInterval). |
| ENC-6 | Non-deterministic encode. Two encodes of the same input produce different output snippets (random seed per encode). |
| ENC-7 | AJAX-safe injection. The snippet does not require being parsed at initial page load; it works when injected into a live DOM (rules out post-load document.write). |
A reasonable v1 design — informed by the reference Ruby/PHP code but not copying it — is:
- Encode
sas an array of integers offset by a per-encode randomk(e.g.,s.codePointAt(i) + k, withkin [3, 250]). - Optionally apply 1–N additional reversible transforms chosen at random from a pool (reverse, pairwise swap, XOR with a per-encode mask). Record the sequence used.
- Emit a
<script>that contains the array literal, the randomk, the transform sequence, and a small decoder loop. The decoder reassemblessand writes it into the DOM by replacing a sentinel<span>whose text is the fallback message. - The static snippet contains the array of numbers and the decoder
source. It does not contain the email address, and contains no
@.
This is one acceptable shape. Implementers may also choose recursive
eval chains (per the original), Function(...) constructors, or
base64+XOR; any choice that meets §6.1 is fine.
Before showing the result, the app SHOULD assert ENC-1, ENC-2, ENC-3 on the snippet it just produced:
- ENC-1. Execute the decoder in a sandboxed function against a faked DOM and confirm the recovered string equals the input verbatim.
- ENC-2. Confirm the static text contains no occurrence of the input
string. When the input is a
mailto:link, also confirm the bare email address does not appear. The full adjacency clause of ENC-2 is not separately checked at runtime: the encoder transforms every input code point into a number, so the local-part cannot appear adjacent to the domain in static text unless the entire input does — which is already checked. - ENC-3. Confirm the static text contains no
@character.
If any assertion fails, show an error and do not display output. The self-check exists to make regressions in the encoder loud rather than silent. It runs in the Obfuskoder app at encode time; it is not bundled into the snippet that the user pastes elsewhere.
- The deliverable is one
index.htmlfile. - All CSS lives in an inline
<style>block. All JavaScript lives in inline<script>blocks. No<link rel="stylesheet">, no<script src=...>, no fonts loaded from a CDN, no images loaded from a CDN. - No external modules, libraries, packages, or frameworks at runtime.
No React/Vue/Svelte/jQuery/Alpine, no utility libraries, no
import/importmap, no npm/yarn/pnpm install step. The shipped page uses only the platform: HTML, CSS, and browser-native JavaScript APIs. - No build step. The file is hand-edited and shipped as-is. No bundler, transpiler, minifier, or preprocessor sits between source and release. ("View source" on the live page === the source of truth.)
- Development-time tools (formatter, axe-core for accessibility checks, browser devtools) are fine — they do not ship with the page.
- The file MUST be openable from
file://and function fully offline. - Any logo or icon is an inline SVG or omitted.
- Target page weight: under 50 KB uncompressed.
- Every form control has a programmatically associated
<label>. - Focus order follows visual order; visible focus styles (
:focus-visible) pass 3:1 contrast. - Color contrast: text against background ≥ 4.5:1; large text and UI controls ≥ 3:1. Verified in both light and (if implemented later) dark themes.
- The tab control is a real ARIA tablist with arrow-key navigation, or is implemented as two visible side-by-side regions on wide screens so no tab pattern is needed.
- Errors are associated to their field via
aria-describedbyand the field is markedaria-invalidwhen errored. - "Copied" and other transient confirmations are announced via an
aria-live="polite"region. - Page is usable with the keyboard alone; no mouse-only paths.
- No content that flashes more than 3 times per second.
- The preview iframe has a meaningful
titleattribute.
- Latest two stable versions (as of release) of Chrome, Firefox, Safari, Edge.
- Mobile Safari and Chrome Android on currently supported OS versions.
- Graceful degradation: in a browser without
navigator.clipboard, fall back to the manual-select-and-copy path described in §5.4. - No IE support. No transpilation step (we ship modern JS as-is).
- First contentful paint < 100 ms on a cold load of
file://. - Encoding the longest reasonable input (advanced form, up to 4 KB of HTML) completes in < 100 ms on a mid-range laptop.
- No outbound network requests from the page itself.
- No tracking, no analytics, no third-party fonts/CDNs.
- User input is held in memory only; not written to
localStoragein v1. - The preview iframe is
sandbox="allow-scripts"(noallow-same-origin) so user-supplied HTML in advanced mode cannot reach the host context.
- UI copy is English in v1, but kept in a small string table near the top of the script so a future translation is a one-file diff.
- Input handling is Unicode-safe end to end. The encoder operates on
Unicode code points (
codePointAt/String.fromCodePoint), not on UTF-16 code units, so emoji and non-BMP characters in user-pasted HTML round-trip correctly.
The look should read as a 2026 web tool, not a 2009 web tool. Reference points: GitHub-style form layout, Vercel-style spacing, system font stack.
- Layout: centered single column, max-width ~720 px, generous vertical spacing. On screens ≥ 960 px, the result area appears beside the form; on narrower screens it appears below.
- Typography:
system-ui, -apple-system, "Segoe UI", Roboto, sans-seriffor UI;ui-monospace, SFMono-Regular, Menlo, Consolas, monospacefor the output textarea and preview HTML. - Color: light theme only in v1. Neutral grays + one accent color for primary action and focus rings.
- No drop shadows beyond a soft elevation on the result card.
- No icon font dependency. Inline SVG for the copy/check icon.
- No splash screen, no modals, no toasts beyond the inline "Copied" confirmation.
A wireframe sketch (ASCII):
+--------------------------------------------------------+
| Obfuskoder |
| Turn an email address into a snippet that bots (maybe) |
| can't read but visitors can. |
+--------------------------------------------------------+
| [ Basic ] [ Advanced ] |
+--------------------------------------------------------+
| Email address [____________________] |
| Link text [____________________] |
| Link title [____________________] (optional) |
| Subject [____________________] (optional) |
| |
| [ Obfuskode ] |
+--------------------------------------------------------+
| Result |
| +--------------------------------------------------+ |
| | <span id="...">Enable JavaScript to view | |
| | email</span><script>...</script> | |
| +--------------------------------------------------+ |
| [ Copy ] |
| |
| Preview |
| +--------------------------------------------------+ |
| | Email me | |
| +--------------------------------------------------+ |
| ▸ Show source HTML |
+--------------------------------------------------------+
The product is releasable when all of the following are true:
- One
index.htmlfile, opens fromfile://, no network requests in DevTools when loaded or operated. - Basic form: valid input produces a snippet whose preview renders
the expected
mailto:link. - Advanced form: arbitrary HTML round-trips through the encoder (preview shows the same rendered output the raw HTML would).
- Encoded snippet contains no
@character and no instance of the input email address (automated assertion §6.3 passes for 50 random inputs in a developer test). - Two consecutive encodes of identical input produce different snippets.
- Copy button writes the snippet to the system clipboard; "Copied" confirmation is announced to screen readers.
- Tab through the entire UI with the keyboard only — every interactive element is reachable, has a visible focus state, and activates with Enter/Space as appropriate.
- Axe (or equivalent) reports zero serious/critical accessibility issues on the page in its initial and post-encode states.
- Page renders without overflow on a 360 px-wide viewport.
- Color contrast passes WCAG AA (verified with one automated tool plus one manual sample).
- Works in latest Chrome, Firefox, and Safari (desktop + mobile Safari).
- Dark mode / system color-scheme support.
- Persisting last-used inputs in
localStorage. - Exposing
max_passes/max_lengthknobs (the original Hivelogic knobs) as user controls. - Multiple algorithm choices selectable by the user.
- Localization beyond English.
- A bookmarklet / browser extension version.
- A "share permalink" feature (would either leak the email into a URL or require a backend; both undesirable).
- Bulk encode (paste many addresses).
- A downloadable static export of the result (just copy is enough).
- Product name: Obfuskoder.
- License: MIT, ship in a
LICENSEfile at repo root. - Project
README.md(separate from this spec) MUST include an acknowledgement of the original Hivelogic Enkoder by Dan Benjamin — the inspiration for this project. Suggested wording: "Obfuskoder is inspired by the original Hivelogic Enkoder by Dan Benjamin, which is sadly no longer online." Link kept for the historical reference even though it no longer resolves; a footnote may also reference the Wayback Machine capture. - Fallback message (default): "Enable JavaScript to view email" — generic on purpose so the Obfuskoder name does not travel into every consumer site that uses an encoded snippet. Held in the script's string table so a later version can let the user override it per encode.
- Deployment URL. Static-host-agnostic. If a canonical URL is
chosen, the page
<title>,<meta name="description">, and any Open Graph tags should be set to match. v1 ships with neutral defaults if no URL is decided.
Enkoder-Forms.png— screenshot of the original Hivelogic web form. Used here for input field set reference only; visual design is not inherited.Hivelogic - The Enkoder Form.webarchive,Hivelogic - The Anti-Spam Email Address Enkoder Web Form.webarchive— archived original pages.enkoder-master/lib/enkoder.rb— original Ruby implementation (Hivelogic, 2007).phpenkoder-trunk/enkoder.php— WordPress port (Greenberg, 2014).standalone-enkoder/StandalonePHPEnkoder.php— standalone PHP port, notable for the AJAX-safeouterHTMLinjection pattern (Nicol, 2015).Enkoder.app/— a 2009 Mac desktop app version of the same tool.
These are read-only references. No code from them is shipped in v1.