← Back to blog
发邮件验证码OTPSMTP开发者

Sending OTP codes by email: a complete implementation guide

How to send one-time passcodes by email using SMTP or a transactional email API, with code examples for secure random generation, Redis-backed validation, and rate limiting.

Updated October 12, 2026
Sending OTP codes by email: a complete implementation guide

Sending a one-time passcode (OTP) by email is a straightforward authentication step: the user registers or signs in, the server sends a six-digit code, and the user enters it to verify ownership of the address. Getting the implementation right involves choosing the right sending path, generating codes safely, and storing and validating them correctly.

Sending options

Transactional email APIs (Resend, SendGrid, Postmark): purpose-built for programmatic sending, with REST or SDK interfaces, high throughput, and built-in deliverability tooling. The right choice for production at scale. They have a cost and their latency from mainland China varies by provider.

SMTP relay: send through your own domain mailbox or a provider-supplied relay. Works well for low-to-moderate volume, or when you already have mail hosting. Rovela mailboxes expose a standard SMTP endpoint.

Sending with Rovela SMTP

Connection settings:

  • Host: mx.rovela.dev
  • Port: 465 (SSL/TLS) or 587 (STARTTLS)
  • Username: your email address
  • Password: an app-specific password generated in the dashboard

Node.js example using nodemailer:

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  host: 'mx.rovela.dev',
  port: 465,
  secure: true,
  auth: {
    user: 'noreply@yourcompany.com',
    pass: process.env.SMTP_PASS, // never hard-code
  },
});

async function sendOtp(to, code) {
  await transporter.sendMail({
    from: '"Your App" <noreply@yourcompany.com>',
    to,
    subject: 'Your verification code',
    text: `Your code is ${code}. It expires in 5 minutes.`,
    html: `<p>Your code is <strong>${code}</strong>. It expires in 5 minutes.</p>`,
  });
}

Generating OTPs

Use a cryptographically secure source, not Math.random():

const crypto = require('crypto');

function generateOtp(length = 6) {
  const max = Math.pow(10, length);
  const buf = crypto.randomBytes(4);
  return String(buf.readUInt32BE(0) % max).padStart(length, '0');
}

Storing and validating

Keep the code server-side (database or Redis) with an expiry. At validation time:

  1. Check the code exists and has not expired (5–10 minutes is typical)
  2. Compare in constant time to prevent timing attacks
  3. Delete the code immediately after a successful match (single use)
  4. Rate-limit sends per address to prevent abuse

Redis example:

const redis = require('redis');
const client = redis.createClient();

async function storeOtp(email, code, ttlSeconds = 300) {
  await client.set(`otp:${email}`, code, { EX: ttlSeconds });
}

async function verifyOtp(email, inputCode) {
  const stored = await client.get(`otp:${email}`);
  if (!stored) return { ok: false, reason: 'expired' };
  const match = crypto.timingSafeEqual(
    Buffer.from(stored), Buffer.from(inputCode)
  );
  if (!match) return { ok: false, reason: 'wrong_code' };
  await client.del(`otp:${email}`);
  return { ok: true };
}

Rate limiting sends

One code per address per minute keeps you from being used to spam someone else's inbox and protects your sending quota:

async function canSendOtp(email) {
  const key = `otp_lock:${email}`;
  const set = await client.set(key, '1', { NX: true, EX: 60 });
  return set !== null; // null means the key already existed
}