Skip to content

Instantly share code, notes, and snippets.

Last active December 7, 2023 15:53
Show Gist options
  • Save vkz/c966a71a00343139d71bab880479227d to your computer and use it in GitHub Desktop.
Save vkz/c966a71a00343139d71bab880479227d to your computer and use it in GitHub Desktop.
Simplifying web forms with one `mailto:` trick

mailto: is all you need

Every time I touch frontend and do any sort of programming for the web, it leaves me near tears. Moment I venture out of the cozy backend into the insanity of the whole client - server interaction ... yaks - yaks everywhere. Latest offender is, well, forms. I feel like much of the well designed things happened early when www was only just making an appearance and since then we've squandered it. Or the designs ended falling on their faces when confronted with evil agents and spam.

As I see it, there are four primary types of user interactions through forms:

  1. Email Collection Forms: Simple forms where users provide their email for updates or contact.
  2. Context-Aware Email Forms: A step further, where users give additional context for a more tailored email response.
  3. CRUD Operations: Basic forms for database updates.
  4. Interactive Journeys: Complex forms that engage users in a more dynamic exchange.

I honestly stuggle to think of anything beyond those. We'll only deals with the first two above, cause, well, that was what I needed. Both of these cases, in fact.

How do we handle that usually? Well, we collect the address, then send em a magic link with token to confirm the address, or smth. IIUC most people these day would pay some other service these days. It's like totally a thing and a viable business. If this isn't a condemnation of the state of things I don't know what is. I mean that magic link or token needs to be checked, so, state, so even for a trivial use case such as this you gotta have a database. The hell?!

I spent some time pondering my options. Well, couldn't I just use the old-school href="" link? That scheme has got to be as old as the Web itself. Idea is that when you click such a link your default email client should pop up. Best part, at least according to the spec, you can pass it query parameters and thus pre-populate email fields like Subject, CC, BCC and best of all Body. Yes, really.

Taking this further, you can totaly use "mailto:..." as a form action attribute. Name your fields appropriately e.g. subject and body, set form method to GET and in theory you have yourself a template for an email your user or customer sends you using their default email client. Well, kinda. It should work in theory but falls flat in practice cause we totally fucked the practice up.

Here're the two wrinkles.

Let's deals with the GET + mailto action trick first. Yeah, that will work, but only kind of. I mean you can hack it, but trouble is that query part of the mailto requires params to be URL encoded, not form encoded. What's the difference, you ask? Spaces can be replaced by %20 or by +. Recipients or whoever parses these should be permissive or so I here, well, not in real life they aren't. I'm sure some clients will handle the difference gracefully, but sadly if you try this in iOS, the mail client will open and populate subject and body parts but you'll have + eveywhere. I guess that's out. Not flexible enough anyway, cause we likely want to collect a few fields into our message body anyway. Would've been a cool hack, though.

Second wrinkle is I guess less subtle and more unfortunate.

Why do I event want to go this route in the first place? Why not just do what everyone else does? Simple answer is that everyone else is stupid (tm) complicit or rather gotten used to overcomplicating things and not thinking them through. I mean the whole token magic link - "please, valued customer, check your email to confirm" - will work. I'm sure. Kinda, unless initial email ends up in spam folder. Anyway, let's just think about it.

What do we want? A way to reach the user via email.

What's the problem? Spam bots and email harvesters is the problem.

This is why we can't have nice and simple things, cause the olden web was def quorky but pleasantly simple which is often reflected in protocols - certainly not ideal, but also not the batshit crazy stuff w3c standards committee puts out, nor our ad-driven overlords (glaring at you Chrome). Basically, you want to make sure whoever's submitting this form is human. They very well may turn out to be a shitty human and a spammer, but unlikely. It is all automated these days. Chances of you encountering mechanical turk type spammer harvesting emails and spaming your forms are slim.

Take mailto: scheme or, I guess, protocol. Why is it nice? Cause by design it assumes the person click on that link, then send you that email from their email client. Not only is it a manual process, but they are the ones initiating the conversation. Added benefit is that, since they are the ones sending you an email first, your receiving address and likely custom domain won't end up on the bad side of their anti-spam filters. At least theoretically. It is all dark magic these days, so anything can happen.

Problem is - you can't count on the default email client being configured in the browser or more generally in their OS. You can do this easily on a Mac and Windows and I'm sure on Linux (by spending a sleepless night reading reddit and support forums). In Chrome you can quickly check what's up by visiting chrome://settings/handlers. Turns out websites can request to become default handlers for custom protocols, mailto: among them - I never knew! This is how you can setup Gmail to handle your mailto clicks, etc etc. If you have it or another client setup, you can delete the default app handling it on this page and experiment.

What happens when the handler isn't set? Well, exactly nothing, and that's just freaking sad. Let me explain and show you what I ended up doing.

You can do the first step on the client, completely in Javascript and avoid a round trip, but I actually prefer bouncing off the server here, cause server-side is always better in my book. So:

  1. you present a form,
  2. user submits that form,
  3. server side you collect all fields
  4. and generate mailto: link that may include e.g. subject and, say, rest of the form params nicely rendered as if in a message body, these go into body param URL encoded, of course.
  5. you then return your user a page with a simple form (or whatever) - page that shows subject of the message, body of the message (I disable altering these) and a single button, or rather link that has its href set to our generated mailto: string.

This entire sequence could've been done on the client without the round trip, but I'm old school. Notice, that nowhere here we need the database - no need to keep the state around like a unique token or smth. Ideally, your user now clicks the Send button (or link), their email client pops up with message prefilled with all necessary fields. Wonderful! Except, if one isn't configured precisely nothing will happen. No event, nothing for you to work with. I hear once upon a time Chrome (probably others, too) used to have some flag your client could consult via Javascript. Now? You can ask to register your site as a handler for custom protocol like mailto, but you can't freaking check if the user has one setup. Everyone is fired.

What are we to do? Well, I quite like this way of interaction. I did the dumbest thing I could think of. By the button I leave a note to the user explaining what should happen and if this doesn't happen that they should just copy the message body above and send an email to the following address - followed by my email. Not perfect but it requires about the same amount of effort as checking your email and clicking the silly magic link. You can make it a bit easier on poor users with their systems misconfigured by adding onclick="" to your email address, subject and body fields on the final form. Copying these becomes trivial.

Here's how the intermediate form looks like. Forgive the hiccup notation - I'm allergic to XML. You can move it out into a separate function that generates custom forms and use it anywhere you need such mail-to-me interaction:

[:form {:action "#" :method :post}

 ;; Subject
  [:label {:for "subject"}
   {:rows "1"
    :onclick ""
    :readonly true
    :name "subject"
    :value subject}

 ;; Body
  [:label {:for "body"} "Message body"]
    {:rows "4"
     :onclick ""
     :readonly true
     :name "body"
     :value body}

 ;; Send button
  [:a {:data-turbo "false"
       :target "_top"
       :rel "noopener noreferrer"
       :href mailto}

 ;; Explanation
   "Nothing happens when you click? Your system doesn't have the
    default email app set. No worries. Just email the text above to:"]
  ;; email address to copy
    {:rows "1"
     :onclick ""
     :readonly true
     :name "mailto"
     :value to}

One benefit of this intermediate page or form is that harvesting my email becomes a bit harder. Remember this first step - user filling out the form? There is no mailto: there for email harvesters to collect. POSTing that form returns a page that has the mailto:. I suspect that this should have some protection from at least naive crawlers. I'm not well versed here, so I maybe way off. But feels right.

Just for kicks and to weed out naive spammers you can add some memorable word to the message body, then filter incoming email - anything without that word goes to spam or gets nuked. I just add PINKY_PROMISE_IAM_HUMAN - feels like it'll do the job.

What do you think? Yay? Nay? Good idea? Bad?


Any comments?

As always this hoot is nothing more than a GitHub gist. Please, use its respective comments section if you have something to say. Till next time.


I run a (not only) Clojure SWAT team in London at If you're hiring - I'm looking for my next gig. Check out my CV and shoot an email to vlad at Happy to branch out into JS, Typescript, Go, Erlang, C, C#, F# even Java unless you're stuck below Java'18.

I also launched - the easiest way to start your tech blogging journey. GitHub gist is all you need! Go check it out and subscribe right now.

Follow me on Twitter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment