JavaScript Mailto Link Implementation Guide

Complete guide to generating mailto links in JavaScript with modern frameworks and best practices.

Quick Start (Vanilla JavaScript)

function createMailto({ to, subject = '', body = '', cc = '', bcc = '' }) {
  const params = [];
  
  if (subject) params.push(`subject=${encodeURIComponent(subject)}`);
  if (body) params.push(`body=${encodeURIComponent(body)}`);
  if (cc) params.push(`cc=${encodeURIComponent(cc)}`);
  if (bcc) params.push(`bcc=${encodeURIComponent(bcc)}`);
  
  const query = params.length ? `?${params.join('&')}` : '';
  return `mailto:${to}${query}`;
}

// Usage
const mailto = createMailto({
  to: '[email protected]',
  subject: 'Bug Report',
  body: 'Description:\n- Issue: Login failed\n- Browser: Chrome'
});

console.log(mailto);
// mailto:[email protected]?subject=Bug%20Report&body=Description%3A%0A-%20Issue%3A%20Login%20failed%0A-%20Browser%3A%20Chrome

React Implementation

Custom Hook

import { useMemo } from 'react';

function useMailto({ to, subject = '', body = '', cc = '', bcc = '' }) {
  return useMemo(() => {
    const params = [];
    if (subject) params.push(`subject=${encodeURIComponent(subject)}`);
    if (body) params.push(`body=${encodeURIComponent(body)}`);
    if (cc) params.push(`cc=${encodeURIComponent(cc)}`);
    if (bcc) params.push(`bcc=${encodeURIComponent(bcc)}`);
    
    const query = params.length ? `?${params.join('&')}` : '';
    return `mailto:${to}${query}`;
  }, [to, subject, body, cc, bcc]);
}

// Usage in component
function ContactButton() {
  const mailto = useMailto({
    to: '[email protected]',
    subject: 'Website Inquiry',
    body: 'Hi team,\n\nI have a question about...'
  });
  
  return (
    <a href={mailto} className="btn btn-primary">
      📧 Contact Us
    </a>
  );
}

Complete React Component

import { useState } from 'react';

function MailtoBuilder() {
  const [fields, setFields] = useState({
    to: '',
    subject: '',
    body: '',
    cc: '',
    bcc: ''
  });
  
  const mailto = useMailto(fields);
  const charCount = mailto.length;
  const isOverLimit = charCount > 1800;
  
  const handleChange = (e) => {
    setFields({
      ...fields,
      [e.target.name]: e.target.value
    });
  };
  
  return (
    <div className="mailto-builder">
      <input
        type="email"
        name="to"
        placeholder="To: [email protected]"
        value={fields.to}
        onChange={handleChange}
        required
      />
      
      <input
        type="text"
        name="subject"
        placeholder="Subject"
        value={fields.subject}
        onChange={handleChange}
      />
      
      <textarea
        name="body"
        placeholder="Message body..."
        value={fields.body}
        onChange={handleChange}
        rows={6}
      />
      
      <div className={`char-count ${isOverLimit ? 'warning' : ''}`}>
        {charCount} / 1800 characters
        {isOverLimit && ' ⚠️ Too long for Outlook'}
      </div>
      
      <a 
        href={mailto}
        className="btn btn-primary"
        onClick={(e) => {
          if (!fields.to) {
            e.preventDefault();
            alert('Please enter recipient email');
          }
        }}
      >
        Open Email Client
      </a>
    </div>
  );
}

Vue.js Implementation

Composition API

<template>
  <div class="mailto-form">
    <input v-model="to" type="email" placeholder="To" required />
    <input v-model="subject" placeholder="Subject" />
    <textarea v-model="body" placeholder="Message" rows="6"></textarea>
    
    <div class="preview">
      <strong>Mailto URL:</strong>
      <code>{{ mailto }}</code>
    </div>
    
    <a :href="mailto" class="btn">Send Email</a>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const to = ref('');
const subject = ref('');
const body = ref('');

const mailto = computed(() => {
  const params = [];
  
  if (subject.value) {
    params.push(`subject=${encodeURIComponent(subject.value)}`);
  }
  if (body.value) {
    params.push(`body=${encodeURIComponent(body.value)}`);
  }
  
  const query = params.length ? `?${params.join('&')}` : '';
  return `mailto:${to.value}${query}`;
});
</script>

TypeScript Version

interface MailtoOptions {
  to: string;
  subject?: string;
  body?: string;
  cc?: string;
  bcc?: string;
}

interface MailtoResult {
  url: string;
  length: number;
  isValid: boolean;
  warnings: string[];
}

function createMailto(options: MailtoOptions): MailtoResult {
  const { to, subject = '', body = '', cc = '', bcc = '' } = options;
  
  const params: string[] = [];
  const warnings: string[] = [];
  
  if (subject) params.push(`subject=${encodeURIComponent(subject)}`);
  if (body) params.push(`body=${encodeURIComponent(body)}`);
  if (cc) params.push(`cc=${encodeURIComponent(cc)}`);
  if (bcc) params.push(`bcc=${encodeURIComponent(bcc)}`);
  
  const query = params.length ? `?${params.join('&')}` : '';
  const url = `mailto:${to}${query}`;
  
  // Validation
  if (url.length > 2000) {
    warnings.push('URL exceeds 2000 chars (Outlook may truncate)');
  }
  
  if (url.length > 1800) {
    warnings.push('URL approaching character limit');
  }
  
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const isValid = emailRegex.test(to);
  
  if (!isValid) {
    warnings.push('Invalid email address format');
  }
  
  return {
    url,
    length: url.length,
    isValid,
    warnings
  };
}

// Usage with type safety
const result = createMailto({
  to: '[email protected]',
  subject: 'Hello',
  body: 'Test message'
});

console.log(result);
// {
//   url: 'mailto:[email protected]?subject=Hello&body=Test%20message',
//   length: 58,
//   isValid: true,
//   warnings: []
// }

Node.js / Express Integration

const express = require('express');
const app = express();

app.use(express.json());

// API endpoint to generate mailto
app.post('/api/mailto', (req, res) => {
  const { to, subject, body, cc, bcc } = req.body;
  
  // Validation
  if (!to) {
    return res.status(400).json({ error: 'Recipient email required' });
  }
  
  const params = [];
  if (subject) params.push(`subject=${encodeURIComponent(subject)}`);
  if (body) params.push(`body=${encodeURIComponent(body)}`);
  if (cc) params.push(`cc=${encodeURIComponent(cc)}`);
  if (bcc) params.push(`bcc=${encodeURIComponent(bcc)}`);
  
  const query = params.length ? `?${params.join('&')}` : '';
  const mailto = `mailto:${to}${query}`;
  
  // Check length
  if (mailto.length > 2000) {
    return res.status(400).json({ 
      error: 'Mailto URL too long',
      length: mailto.length,
      max: 2000
    });
  }
  
  res.json({
    success: true,
    mailto,
    length: mailto.length
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Advanced Features

Template System

class MailtoTemplate {
  static support({ product, issue, userId }) {
    const body = `
Hi Support Team,

I'm experiencing an issue with ${product}.

Issue: ${issue}
User ID: ${userId}

Please help!
    `.trim();
    
    return createMailto({
      to: '[email protected]',
      subject: `Support: ${issue}`,
      body
    });
  }
  
  static feedback({ rating, comment }) {
    const stars = '⭐'.repeat(rating);
    const body = `
Rating: ${stars} (${rating}/5)

Feedback:
${comment}
    `.trim();
    
    return createMailto({
      to: '[email protected]',
      subject: 'Product Feedback',
      body
    });
  }
}

// Usage
const supportMailto = MailtoTemplate.support({
  product: 'Web App',
  issue: 'Login Error',
  userId: 'USER-123'
});

Builder Pattern

class MailtoBuilder {
  constructor(to) {
    this.to = to;
    this.params = {};
  }
  
  setSubject(subject) {
    this.params.subject = subject;
    return this;
  }
  
  setBody(body) {
    this.params.body = body;
    return this;
  }
  
  addCC(cc) {
    this.params.cc = cc;
    return this;
  }
  
  addBCC(bcc) {
    this.params.bcc = bcc;
    return this;
  }
  
  build() {
    return createMailto({ to: this.to, ...this.params });
  }
  
  getStats() {
    const url = this.build();
    return {
      length: url.length,
      maxLength: 2000,
      remaining: 2000 - url.length,
      percentage: (url.length / 2000 * 100).toFixed(1)
    };
  }
}

// Usage
const mailto = new MailtoBuilder('[email protected]')
  .setSubject('Bug Report')
  .setBody('Description here...')
  .addCC('[email protected]')
  .build();

console.log(mailto);

Analytics Tracking

// Track mailto clicks with Google Analytics 4
function trackMailtoClick(email, label) {
  if (typeof gtag !== 'undefined') {
    gtag('event', 'mailto_click', {
      'event_category': 'contact',
      'event_label': label,
      'email_address': email
    });
  }
}

// Usage
document.querySelectorAll('a[href^="mailto:"]').forEach(link => {
  link.addEventListener('click', () => {
    const email = link.href.replace('mailto:', '').split('?')[0];
    trackMailtoClick(email, window.location.pathname);
  });
});

Testing

// Jest tests
describe('Mailto Generation', () => {
  test('creates basic mailto', () => {
    const result = createMailto({ to: '[email protected]' });
    expect(result).toBe('mailto:[email protected]');
  });
  
  test('encodes subject', () => {
    const result = createMailto({ 
      to: '[email protected]',
      subject: 'Hello World'
    });
    expect(result).toContain('subject=Hello%20World');
  });
  
  test('encodes line breaks', () => {
    const result = createMailto({ 
      to: '[email protected]',
      body: 'Line 1\nLine 2'
    });
    expect(result).toContain('%0A');
  });
  
  test('handles special characters', () => {
    const result = createMailto({ 
      to: '[email protected]',
      subject: 'Test & Demo'
    });
    expect(result).toContain('%26');
  });
});

Common Pitfalls

// ❌ WRONG: Not encoding
const mailto = `mailto:${to}?subject=${subject}`;

// ✅ CORRECT: Proper encoding
const mailto = `mailto:${to}?subject=${encodeURIComponent(subject)}`;

// ❌ WRONG: Using escape() (deprecated)
const mailto = `mailto:${to}?subject=${escape(subject)}`;

// ✅ CORRECT: Use encodeURIComponent
const mailto = `mailto:${to}?subject=${encodeURIComponent(subject)}`;

// ❌ WRONG: Forgetting to join params with &
const mailto = `mailto:${to}?subject=${subject}?body=${body}`;

// ✅ CORRECT: Use & for additional params
const mailto = `mailto:${to}?subject=${subject}&body=${body}`;

Browser Compatibility

// Check if mailto is supported
function supportsMailto() {
  return 'mailto:' in window.location;
}

// Fallback for unsupported browsers
function handleMailto(email) {
  if (supportsMailto()) {
    window.location.href = `mailto:${email}`;
  } else {
    // Copy to clipboard as fallback
    navigator.clipboard.writeText(email);
    alert(`Email copied: ${email}`);
  }
}

📚 Mailto Guides

💻 Other Language Guides

📖 Deep Dives


Test your implementation with our free mailto link generator to ensure cross-browser compatibility.

← Back to Resources | Try Generator →

Have feedback? We'd love to hear it!