Skip to content

Support mailto: destinations (email link shortening) with a compose UI #441

Description

@mfcarroll

One of my team asked if our rushomon instance could support mailto links. They currently use linkly for that but would prefer our url branding. I'm happy to build this. Proposed plan below is AI, based on how linkly works, but revised and reviewed. Let me know if you think this fits, as I'd rather not maintain a fork of the link creation modal etc if this isn't wanted upstream. Thanks!

Use case

Shorten a mailto: URL (address + subject/body/cc/bcc) so a short link opens the user's mail client with a pre-filled compose window. Useful for print/QR contexts ("scan to email us"), signatures, and campaign CTAs. Hand-writing a correctly percent-encoded mailto: URL is error-prone, so this includes a small compose UI. Linkly and Short.io support mailto shortening; Bitly doesn't.

Current behaviour

validate_url() rejects anything that isn't http/https, so mailto: destinations can't be created, and there's no UI to build one.

Proposed behaviour

Backend

  1. Validation: accept mailto: alongside http / https (create/update/import share validate_url, covering every entry point). Validate structurally: non-empty comma-separated address list, query params allowlisted to subject, body, cc, bcc (closes off RFC 6068's arbitrary-header loophole), cc/bcc validated as address lists. javascript:/data:/file: stay rejected.
  2. Serving: Location: mailto: redirects are unreliable across browsers/in-app webviews, so when the resolved destination is mailto:, serve a 200 HTML interstitial instead of a 30x: immediate JS redirect + meta refresh fallback + a visible "Compose email to address" link (which also gives visitors transparency about where the link points). noindex, no-store. Analytics/click counting attach exactly as today.
  3. Skip UTM/query forwarding for mailto destinations — appending params would corrupt the compose window.
  4. Blacklist: extend domain-match to extract the domain from the email address (host_str() is None for mailto), so admins can block abusive addresses/domains.

Frontend

  1. Link-type selector: a compact Web | Email segmented control in the link modal's destination section, defaulting to Web — the regular-link flow is visually and behaviourally unchanged. Pasting a mailto: string auto-switches to Email mode; editing an existing mailto link opens in Email mode.
  2. Email mode: To (required, multiple comma-separated), Subject, Body, and Cc/Bcc behind a collapsed toggle. The form builds a canonical, correctly-encoded mailto: string submitted as destination_urlno API changes; the composer is a pure convenience layer over the same field, and round-trips (parse ↔ build) for editing.
  3. Display: envelope icon in place of the favicon for mailto links; address shown where the hostname normally appears. Title auto-suggest becomes "Email address".

Security notes

  • The mailto URL is embedded in an HTML attribute and a JS string on the interstitial — escaped at both insertion points, with tests covering quote/angle-bracket injection via crafted subject=/body= params.
  • Query-param allowlist prevents smuggling arbitrary RFC 6068 header fields.
  • Showing the target address on the interstitial mitigates the phishing-obfuscation concern; existing report + blacklist flows apply.

Out of scope (possible follow-up)

  • tel: / sms: schemes (the scheme branch will be trivially extensible)

No schema/migration changes — destination_url is already free-text and the KV mapping is unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Next Version

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions