AgentSkillsCN

email-sender

通过客户端域名的 SMTP 服务,结合身份验证、速率限制与垃圾邮件防护功能,实现网站的安全邮件发送。适用于各类联系表单、事务性邮件、新闻通讯订阅,以及任何需要邮件功能的场景。需提前配置好具备 SMTP 服务器的域名,并在 .env 文件中录入相应的登录凭证。

SKILL.md
--- frontmatter
name: email-sender
description: Secure email sending from websites using client domain SMTP with authentication, rate limiting, and spam prevention. Use when implementing contact forms, transactional emails, newsletter signups, or any email functionality. Requires domain name with SMTP server and credentials configured in .env file.

Email Sender: Secure Domain SMTP Implementation

Send emails from your website using the client's own domain SMTP server — secure, spam-preventable, and fully authenticated.

Prerequisites

Client Requirements (Non-Negotiable):

  • Domain name owned and controlled by client
  • Access to domain email/SMTP settings (usually via hosting provider or business email service)
  • DKIM, SPF, and DMARC records configured on domain (for deliverability)
  • Dedicated email account for website (e.g., noreply@clientdomain.com or contact@clientdomain.com)

Without these, email security and deliverability will fail.

Architecture

code
Client Request (form submission)
  ↓
Rate Limiter (max 5 submissions per IP per hour)
  ↓
Input Validation + CAPTCHA check
  ↓
Spam Detection (content analysis)
  ↓
SMTP Credentials (loaded from .env, never exposed)
  ↓
Authenticated Email Send (using domain SMTP)
  ↓
Audit Log (request logged for security review)
  ↓
Response to client (success or rate-limited message)

Step 1: Environment Configuration

Create .env.local (never commit to git):

bash
# Email Sender Configuration
SMTP_HOST=mail.clientdomain.com          # SMTP server (ask hosting provider)
SMTP_PORT=587                             # Standard SMTP port (465 for SSL)
SMTP_SECURE=true                          # Use TLS/SSL
SMTP_USER=noreply@clientdomain.com       # Dedicated email account
SMTP_PASSWORD=secure_password_here        # Generated by provider, never hardcode
SMTP_FROM_NAME="Client Company Name"      # Display name in "From" field
SMTP_FROM_EMAIL=noreply@clientdomain.com # Must match SMTP_USER

# Rate Limiting & Security
RATE_LIMIT_EMAILS_PER_HOUR=5             # Max emails per IP per hour
RATE_LIMIT_WINDOW_MINUTES=60             # Rolling window
SPAM_CHECK_ENABLED=true                  # Enable content-based spam detection
CAPTCHA_SECRET_KEY=your_captcha_key      # For bot prevention

# Audit & Monitoring
AUDIT_LOG_PATH=/var/log/website-emails.log
ALERT_ADMIN_ON_RATE_LIMIT=true           # Email admin if rate limit hit
ALERT_EMAIL=admin@clientdomain.com       # Admin email for alerts

CRITICAL SECURITY RULES:

  • .env.local is .gitignored — never commit credentials
  • .env.example shows template (no real values)
  • Credentials stored server-side only — never exposed to client
  • Each project has unique SMTP credentials per domain

Step 2: DKIM/SPF/DMARC Setup (Required for Deliverability)

Before enabling email sending, configure domain authentication:

SPF Record

Tells mail servers which servers are allowed to send email for your domain:

code
DNS TXT Record:
v=spf1 include:_spf.google.com include:mail.clientdomain.com ~all

(Replace with client's email provider SPF record)

DKIM Record

Adds digital signature to emails so they can't be spoofed:

code
DNS CNAME/TXT Record:
selector1._domainkey.clientdomain.com → [generated by email provider]

(Get DKIM record details from hosting provider or email service)

DMARC Record

Policy for what happens when SPF/DKIM fail:

code
DNS TXT Record:
v=DMARC1; p=quarantine; rua=mailto:dmarc@clientdomain.com; ruf=mailto:dmarc@clientdomain.com

Verification:

  • Use MXToolbox.com or similar to verify SPF/DKIM/DMARC setup
  • SPF: "pass" ✓
  • DKIM: "pass" ✓
  • DMARC: "pass" ✓

Without these, emails go to spam.

Step 3: Contact Form Implementation

Backend (Node.js + Express Example)

typescript
// src/api/email.ts (Astro API endpoint)

import nodemailer from 'nodemailer';
import rateLimit from 'express-rate-limit';
import { validateEmail, detectSpam } from '../utils/email-utils';

// Rate limiter: 5 emails per IP per hour
const emailLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 requests per window
  message: 'Too many emails sent. Please try again in an hour.',
  standardHeaders: false,
  skip: (req) => req.clientIP === process.env.ADMIN_IP, // Skip for admin
});

// SMTP Transporter (created once, reused)
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: parseInt(process.env.SMTP_PORT || '587'),
  secure: process.env.SMTP_SECURE === 'true', // true for 465, false for 587
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASSWORD,
  },
});

export async function POST({ request }) {
  try {
    const clientIP = request.headers.get('x-forwarded-for') || 'unknown';

    // 1. Rate limiting
    const limitCheck = emailLimiter.skip
      ? { skip: false }
      : { rateLimitRemaining: 5 };

    if (limitCheck.rateLimitRemaining <= 0) {
      // Log rate limit hit
      console.warn(`Rate limit exceeded from IP: ${clientIP}`);

      return new Response(
        JSON.stringify({
          error: 'Too many submission attempts. Please try later.',
          retry_after: 3600
        }),
        {
          status: 429,
          headers: { 'Content-Type': 'application/json' }
        }
      );
    }

    // 2. Parse form data
    const formData = await request.formData();
    const name = formData.get('name');
    const email = formData.get('email');
    const phone = formData.get('phone');
    const message = formData.get('message');
    const captchaToken = formData.get('captchaToken');

    // 3. Input validation
    if (!name || !email || !message) {
      return new Response(
        JSON.stringify({ error: 'Missing required fields' }),
        { status: 400 }
      );
    }

    if (!validateEmail(email)) {
      return new Response(
        JSON.stringify({ error: 'Invalid email address' }),
        { status: 400 }
      );
    }

    // 4. CAPTCHA verification (reCAPTCHA v3)
    if (process.env.CAPTCHA_SECRET_KEY) {
      const captchaVerified = await verifyCaptcha(
        captchaToken,
        process.env.CAPTCHA_SECRET_KEY
      );

      if (!captchaVerified) {
        return new Response(
          JSON.stringify({ error: 'CAPTCHA verification failed' }),
          { status: 400 }
        );
      }
    }

    // 5. Spam detection (content analysis)
    const spamScore = detectSpam(message);
    if (spamScore > 0.8) {
      console.warn(`High spam score (${spamScore}) from ${email}`);
      // Silently reject but respond as if successful (don't reveal spam detection)
      return new Response(
        JSON.stringify({
          success: true,
          message: 'Your message has been received.'
        }),
        { status: 200 }
      );
    }

    // 6. Prepare email content
    const htmlContent = `
      <h2>New Contact Form Submission</h2>
      <p><strong>Name:</strong> ${sanitize(name)}</p>
      <p><strong>Email:</strong> ${sanitize(email)}</p>
      ${phone ? `<p><strong>Phone:</strong> ${sanitize(phone)}</p>` : ''}
      <p><strong>Message:</strong></p>
      <p>${sanitize(message).replace(/\n/g, '<br>')}</p>
      <hr>
      <p><small>Submitted from: ${clientIP}</small></p>
    `;

    const textContent = `
New Contact Form Submission

Name: ${name}
Email: ${email}
${phone ? `Phone: ${phone}` : ''}

Message:
${message}

---
Submitted from: ${clientIP}
    `;

    // 7. Send email to admin
    const adminMailOptions = {
      from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_FROM_EMAIL}>`,
      to: process.env.CONTACT_EMAIL || process.env.SMTP_USER,
      subject: `New Contact Form: ${name}`,
      html: htmlContent,
      text: textContent,
      replyTo: email,
    };

    await transporter.sendMail(adminMailOptions);

    // 8. Send confirmation email to user
    const userMailOptions = {
      from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_FROM_EMAIL}>`,
      to: email,
      subject: 'We received your message',
      html: `
        <p>Hi ${sanitize(name)},</p>
        <p>Thank you for contacting us. We've received your message and will respond within 24 hours.</p>
        <hr>
        <p><small>This is an automated confirmation email. Please do not reply.</small></p>
      `,
      text: `Hi ${name},\n\nThank you for contacting us. We've received your message and will respond within 24 hours.`,
    };

    await transporter.sendMail(userMailOptions);

    // 9. Audit log
    logEmailSubmission({
      timestamp: new Date(),
      name,
      email,
      ip: clientIP,
      status: 'success',
      spamScore,
    });

    return new Response(
      JSON.stringify({
        success: true,
        message: 'Your message has been sent. We will respond within 24 hours.'
      }),
      {
        status: 200,
        headers: { 'Content-Type': 'application/json' }
      }
    );

  } catch (error) {
    console.error('Email send error:', error);

    // Log error
    logEmailSubmission({
      timestamp: new Date(),
      error: error.message,
      status: 'error',
    });

    // Don't expose error details to user
    return new Response(
      JSON.stringify({
        error: 'Failed to send message. Please try again later.'
      }),
      { status: 500 }
    );
  }
}

// Helper: Verify reCAPTCHA token
async function verifyCaptcha(token, secretKey) {
  try {
    const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
      method: 'POST',
      body: new URLSearchParams({
        secret: secretKey,
        response: token,
      }),
    });

    const data = await response.json();
    return data.success && data.score > 0.5;
  } catch (error) {
    console.error('CAPTCHA verification error:', error);
    return false;
  }
}

// Helper: Sanitize HTML
function sanitize(input) {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// Helper: Spam detection (simple content analysis)
function detectSpam(content) {
  const spamPatterns = [
    /viagra|cialis|casino|lottery|prize|click here|buy now/gi,
    /http[s]?:\/\/[^\s]{20,}/g, // Long URLs
    /<script|<iframe|<img|onclick/gi, // Malicious tags
  ];

  let spamScore = 0;
  spamPatterns.forEach(pattern => {
    const matches = content.match(pattern);
    if (matches) {
      spamScore += matches.length * 0.2;
    }
  });

  return Math.min(spamScore, 1.0);
}

// Helper: Audit logging
function logEmailSubmission(data) {
  const logPath = process.env.AUDIT_LOG_PATH;
  if (logPath) {
    const fs = require('fs');
    fs.appendFileSync(logPath, JSON.stringify(data) + '\n');
  }
}

Frontend (Contact Form with CAPTCHA)

astro
---
// src/components/ContactForm.astro
import ReCAPTCHA from '@astrojs/recaptcha';

const reCaptchaKey = import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY;
---

<form id="contactForm" class="space-y-4" method="POST" action="/api/email">
  <input type="hidden" name="captchaToken" id="captchaToken" />

  <div>
    <label for="name" class="block text-sm font-medium">Name</label>
    <input
      id="name"
      name="name"
      type="text"
      required
      class="w-full px-4 py-2 border rounded"
    />
  </div>

  <div>
    <label for="email" class="block text-sm font-medium">Email</label>
    <input
      id="email"
      name="email"
      type="email"
      required
      class="w-full px-4 py-2 border rounded"
    />
  </div>

  <div>
    <label for="phone" class="block text-sm font-medium">Phone (optional)</label>
    <input
      id="phone"
      name="phone"
      type="tel"
      class="w-full px-4 py-2 border rounded"
    />
  </div>

  <div>
    <label for="message" class="block text-sm font-medium">Message</label>
    <textarea
      id="message"
      name="message"
      rows="5"
      required
      class="w-full px-4 py-2 border rounded"
    ></textarea>
  </div>

  <div data-sitekey={reCaptchaKey} data-action="submit" class="g-recaptcha"></div>

  <button
    type="submit"
    class="px-6 py-2 bg-primary text-white rounded hover:opacity-90"
  >
    Send Message
  </button>
</form>

<script>
  const form = document.getElementById('contactForm');

  if (form) {
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      // Get reCAPTCHA token
      const token = await grecaptcha.execute(
        import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY,
        { action: 'submit' }
      );

      document.getElementById('captchaToken').value = token;

      // Submit form
      const formData = new FormData(form);
      const response = await fetch('/api/email', {
        method: 'POST',
        body: formData,
      });

      const result = await response.json();

      if (response.ok) {
        alert('Message sent successfully!');
        form.reset();
      } else {
        alert(result.error || 'Failed to send message.');
      }
    });
  }
</script>

Step 4: Security Hardening

Never Expose Credentials

typescript
// ❌ WRONG - Credentials exposed to client
export const smtpConfig = {
  user: 'noreply@domain.com',
  password: 'secret123', // EXPOSED!
};

// ✓ CORRECT - Credentials server-side only
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,      // Server-side env var only
  auth: {
    user: process.env.SMTP_USER,    // Never exposed
    pass: process.env.SMTP_PASSWORD, // Never exposed
  },
});

Input Validation

typescript
// Validate all inputs strictly
function validateEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email) && email.length <= 255;
}

function validateName(name) {
  return name.length > 0 && name.length <= 100 && !/[<>]/g.test(name);
}

function validateMessage(message) {
  return message.length > 5 && message.length <= 5000;
}

Rate Limiting Strategy

  • Max 5 emails per IP per hour (configurable)
  • Resets after window expires
  • Admin/trusted IPs can bypass
  • Log all rate limit events

Spam Detection

Combination approach:

  • Content pattern detection (URLs, spam keywords)
  • reCAPTCHA v3 integration (bot detection)
  • SPF/DKIM/DMARC verification (email authentication)
  • User reputation (first-time sender vs. repeat)

Step 5: Deployment Checklist

  • SMTP credentials configured in .env.local
  • .env.local added to .gitignore
  • SPF/DKIM/DMARC records verified at domain
  • Test email sent successfully
  • reCAPTCHA keys obtained from Google Cloud Console
  • Rate limiting tested (try 6 submissions)
  • Spam detection tested with keyword payloads
  • Audit logs configured and verified
  • Admin alert emails tested
  • User confirmation emails tested

Troubleshooting

ProblemCauseSolution
"Authentication failed"Wrong SMTP credentialsVerify credentials with hosting provider
Emails go to spamSPF/DKIM/DMARC not configuredSet up domain authentication records
Rate limit too aggressiveWindow too short or limit too lowAdjust RATE_LIMIT_EMAILS_PER_HOUR
Legitimate emails flagged as spamSpam detection too sensitiveLower spam score threshold or disable
Credentials leaking in logsLogging sensitive dataNever log passwords, only sanitized info

Key Principle: SMTP credentials belong on the server only. Never expose them to the client. Use rate limiting and CAPTCHA to prevent bot abuse.