Why JavaScript in the browser can’t send email
Email travels over SMTP, a protocol that runs on raw TCP connections to ports like 465 and 587. Browsers only let pages make HTTP and WebSocket requests, so front-end JavaScript can’t open an SMTP connection at all. Something on a server has to receive your request over HTTPS and talk SMTP on your behalf.
Your options
| Approach | Good | Not so good |
|---|---|---|
| mailto: link | Zero setup | Opens the visitor’s mail app instead of sending. Many visitors have none set up, and you can’t control what arrives. |
| Browser “SMTP” libraries | Looks like one function call | They relay through a third-party server, and the page needs your SMTP login (or a token tied to it). Anyone who opens DevTools can copy it and send as you, to anyone. |
| Your own serverless function | Full control | You write, deploy and secure backend code, store SMTP secrets on the platform, and build your own spam and rate limits. |
| FormSMTP | One fetch(), sent through your own SMTP, no password in the page | You need an email account with SMTP access (nearly all have one). |
Never put an SMTP password in front-end code, even “obfuscated”. Your page source is public, and a leaked mailbox password is far worse than a leaked form key.
How it works
Your SMTP login stays encrypted in FormSMTP. The page only holds a project key, which you lock to your website, your own inbox and an hourly limit. Your script posts to FormSMTP over HTTPS; FormSMTP checks the request and sends through your SMTP server, so the email comes from your own address.
1Create a FormSMTP project
Sign up free and add a project. Pick your email provider so the SMTP host and port fill in, enter your login, and click Test connection. Not sure which provider? Your existing mailbox works: see the Gmail or Zoho guides.
2Lock the project to your site
In Settings → Security, set Allowed websites to https://yoursite.com and Allowed recipients to your own address. Now the key in your page can only email you, only from your site, and only up to your hourly limit.
3Send from a form
Plain HTML with a little JavaScript for an inline result. Without JavaScript, the form still works and shows a thank-you page.
<form id="contact" action="https://app.formsmtp.com/api/v1/form/YOUR_API_KEY" method="POST">
<input name="name" placeholder="Name" required>
<input name="email" type="email" placeholder="Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
<button type="submit">Send</button>
<p id="status" role="status"></p>
</form>
<script>
const form = document.getElementById("contact");
const status = document.getElementById("status");
const button = form.querySelector("button");
form.addEventListener("submit", async (e) => {
e.preventDefault();
button.disabled = true;
status.textContent = "Sending…";
try {
const res = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new URLSearchParams(new FormData(form)),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
form.reset();
status.textContent = "Thanks! Your message was sent.";
} catch (err) {
status.textContent = "Sorry, that didn't work: " + err.message;
} finally {
button.disabled = false;
}
});
</script>4Or send from any JavaScript
No form on the page? Post JSON to the same endpoint from anywhere in your code: a chat widget, a quiz result, a “notify me” button. Every field that doesn’t start with an underscore ends up in the email.
async function sendEmail(fields) {
const res = await fetch("https://app.formsmtp.com/api/v1/form/YOUR_API_KEY", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(fields),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
return data.messageId;
}
await sendEmail({
_subject: "New quote request",
email: "ana@example.com", // becomes the reply-to address
plan: "Team",
seats: 12,
});The email goes to the address in Allowed recipients (or the project’s From address), with the visitor’s email as reply-to, so you can answer straight from your inbox.
Sending from Node.js or a server
On a server, the key stays secret, so you can use the full send API: any recipient you allow, HTML, templates, cc/bcc and attachments.
const res = await fetch("https://app.formsmtp.com/api/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FORMSMTP_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "customer@example.com",
subject: "Your order has shipped",
html: "<p>Good news: it's on the way.</p>",
}),
});
const data = await res.json(); // { success: true, messageId } or { success: false, error }Handling errors
Every response is JSON with success, and an error message when it fails. A 429 means the project’s hourly limit was reached (check the Retry-After header); a 502 means your SMTP server refused the message, and the error says why. The full list is in the docs.
FAQ
Can JavaScript send email without a server?
Not by itself. Browsers can’t open SMTP connections, so a service has to receive the request over HTTPS and send the email. FormSMTP does that through your own SMTP account, so you don’t run a server.
Is it safe to use my Gmail password in JavaScript?
No. Anything in front-end code is visible to every visitor. Keep the SMTP login on a server; with FormSMTP it’s stored encrypted and the page only holds a key that can email you and nobody else.
Can I send attachments from the browser?
Not through the form endpoint, which doesn’t accept file uploads. The send API accepts base64 attachments, but call it from a server so your key stays private.
Why did my message land in spam?
Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.