Published on

DIY Contact Form: Next.js, Nodemailer, and Protonmail

Authors
  • avatar
    Name
    Johanness Nilsson
    Mastodon
RON BURGUNDY MEME

There are lots of shitty anti-solutions

Look, we've been there before. You're building a slick new website for a client (or yourself), and everything's going great until you hit the contact form. You look for a pre-existing solution, there are dozens of services claiming to be the best, they all cost money, each one promising to do the task, but delivering a complex, overengineered anti-pattern that feels like using a sledgehammer to crack a nut.

Recently found myself in this exact situation. Just needed a simple contact form for a medium-traffic website, and the usual suspects - Sendgrid, Mailgun, Mailchimp - all felt like overkill. Not to mention, i wasn't thrilled about the idea of sharing my users' data with yet another third-party service, esp. with these jokers who can't even come up with a good name for their junk.

when i was looking at one of these "service providers" horrible ui, i almost puked, and that's when it hit me: why am i not just using SMTP on Protonmail? It can be set up secure, and very privacy-focused, and i already had an account. The biggest upside, it would give me full control over the users' data. So, i cracked a cold one and opened a new terminal tab.

My Setup: Next.js, nodemailer, and Protonmail

Here's what i planned on using:

  • Next.js, provides the best React framework
  • nodemailer for sending emails with, uh... node.
  • Protonmail's SMTP server for our email service

First things first, i set up me environment variables. First by creating a .env.local file in the project root:

btw; yes you were supposed to read that last sentence like a Leprechaun, aferall we're getting drunk whilst we chank

// .env.local
PROTON_SMTP_USER=your.email@protonmail.com
PROTON_SMTP_TOKEN=your-app-specific-password
PROTON_SMTP_SERVER=smtp.protonmail.ch
PROTON_SMTP_PORT=587

i also add these to the GitLab CI/CD for deployment into the live environment.

nodemailer

Then i create an API route. In pages/api/submit-form.js:

import nodemailer from 'nodemailer';

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const transporter = nodemailer.createTransport({
    host: process.env.PROTON_SMTP_SERVER,
    port: process.env.PROTON_SMTP_PORT,
    secure: false, // It actually uses STARTTLS, there are no shared keys
    auth: {
      user: process.env.PROTON_SMTP_USER,
      pass: process.env.PROTON_SMTP_TOKEN
    },
    tls: {
      ciphers: 'SSLv3',
      rejectUnauthorized: true,
    }
  });

  const { name, email, message } = req.body;

  try {
    await transporter.sendMail({
      from: process.env.PROTON_SMTP_USER,
      to: 'your.destination@email.com',
      subject: 'New Contact Form Submission',
      text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
    });

    res.status(200).json({ message: 'Email sent successfully' });
  } catch (error) {
    console.error('Error sending email:', error);
    res.status(500).json({ message: 'Error sending email' });
  }
}

The Front-End: A basic-ass React Form

This is just a basic example to work off of:

import { useState } from 'react';

export default function ContactForm() {
  const [formData, setFormData] = useState({ name: '', email: '', message: '' });

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch('/api/submit-form', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });
      if (response.ok) {
        alert('Message sent successfully!');
        setFormData({ name: '', email: '', message: '' });
      } else {
        throw new Error('Failed to send message');
      }
    } catch (error) {
      alert('Error sending message. Please try again.');
    }
  };

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Your Name"
        required
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Your Email"
        required
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Your Message"
        required
      />
      <button type="submit">Send</button>
    </form>
  );
}

Simple is good, and it works.

Yup, a working no bullshit contact form, and i still have half a beer left. Simple, efficient set-up that doesn't rely on any scam-ass third-party email services. It's perfect for low to mid-volume websites, and it offers complete control over users' data. Of course, this solution might not be suitable for high-traffic sites or market bros who need advanced email tracking features. But that ain't me, so the shoe fits just right. Don't forget to add some style your form. Also, you probs wanna add proper form validations and error handling. But you knew that, right?