Skip to main content

Virtual Mailing Lists

EmailEngine's virtual mailing lists provide List-Unsubscribe functionality for bulk email campaigns without requiring a full-featured mailing list manager. Focus on your core application while EmailEngine handles unsubscribe management.

Overview

What Are Virtual Mailing Lists?

Virtual mailing lists are a lightweight feature that provides only the unsubscribe functionality:

What's included:

  • List-Unsubscribe header generation
  • Unsubscribe link hosting
  • One-click unsubscribe (RFC 8058)
  • Unsubscribe webhook notifications

What's NOT included:

  • Contact list management
  • Segmentation rules
  • Sending history
  • Analytics and reporting
  • Bounce management
  • Template management

Virtual lists are perfect when you already have contact management in your application but need to comply with unsubscribe requirements for bulk emails.

Why Use Virtual Lists?

Legal compliance:

  • Required by CAN-SPAM Act (USA)
  • Required by GDPR (EU)
  • Required by CASL (Canada)
  • Expected by email service providers

Deliverability benefits:

  • Gmail requires List-Unsubscribe for bulk senders
  • Improves sender reputation
  • Reduces spam complaints
  • Better inbox placement

User experience:

  • One-click unsubscribe in email clients
  • No need to visit external website
  • Instant unsubscribe processing

How It Works

Sending Flow

List-Unsubscribe Headers

EmailEngine automatically adds these headers to emails sent with a listId:

List-ID: <newsletter-weekly.example.com>
List-Unsubscribe: <https://emailengine.example.com/unsubscribe?data=abc123&sig=xyz>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

One-Click Unsubscribe (RFC 8058):

  • HTTPS-based unsubscribe - Preferred by Gmail and modern email clients
  • Instant processing when recipient clicks unsubscribe

Sending Emails with Virtual Lists

Basic Mail Merge with listId

Use Mail Merge with a listId parameter:

curl -XPOST "http://localhost:3000/v1/account/sender@example.com/submit" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "Weekly Newsletter - {{date}}",
"text": "Hello {{name}},\n\nHere is this week'\''s newsletter...\n\n{{content}}",
"listId": "newsletter-weekly",
"mailMerge": [
{
"to": {"address": "john@example.com"},
"params": {
"name": "John",
"date": "October 13, 2024",
"content": "Newsletter content here..."
}
},
{
"to": {"address": "jane@example.com"},
"params": {
"name": "Jane",
"date": "October 13, 2024",
"content": "Newsletter content here..."
}
}
]
}'

Key field: listId - Identifies this as a virtual mailing list campaign.

Implementation Example

// Pseudo code - implement in your preferred language

function send_newsletter(recipients, subject, content):
// Build merge list
merge_list = []

for each recipient in recipients:
APPEND(merge_list, {
to: { address: recipient.email },
params: {
name: recipient.name,
date: FORMAT_DATE(CURRENT_DATE())
}
})
end for

// Make HTTP POST request
response = HTTP_POST(
'http://localhost:3000/v1/account/sender@example.com/submit',
headers={
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body=JSON_ENCODE({
subject: subject,
text: content,
listId: 'newsletter-weekly', // Enable virtual list
mailMerge: merge_list
})
)

return PARSE_JSON(response.body)
end function

// Usage
send_newsletter(
[
{ email: 'john@example.com', name: 'John' },
{ email: 'jane@example.com', name: 'Jane' }
],
'Weekly Newsletter',
'Hello {{name}}, here is your newsletter for {{date}}...'
)

Python Example

import requests

def send_newsletter(recipients, subject, content):
response = requests.post(
'http://localhost:3000/v1/account/sender@example.com/submit',
headers={
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
json={
'subject': subject,
'text': content,
'listId': 'newsletter-weekly',
'mailMerge': [
{
'to': {'address': r['email']},
'params': {
'name': r['name'],
'date': '2024-10-13'
}
}
for r in recipients
]
}
)

return response.json()

# Usage
send_newsletter(
[
{'email': 'john@example.com', 'name': 'John'},
{'email': 'jane@example.com', 'name': 'Jane'}
],
'Weekly Newsletter',
'Hello {{name}}, here is your newsletter for {{date}}...'
)

PHP Example

<?php

function sendNewsletter($recipients, $subject, $content) {
$mergeList = array_map(function($recipient) {
return [
'to' => ['address' => $recipient['email']],
'params' => [
'name' => $recipient['name'],
'date' => date('Y-m-d')
]
];
}, $recipients);

$data = [
'subject' => $subject,
'text' => $content,
'listId' => 'newsletter-weekly',
'mailMerge' => $mergeList
];

$ch = curl_init('http://localhost:3000/v1/account/sender@example.com/submit');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer YOUR_TOKEN',
'Content-Type: application/json'
]);

$response = curl_exec($ch);
curl_close($ch);

return json_decode($response, true);
}

// Usage
sendNewsletter(
[
['email' => 'john@example.com', 'name' => 'John'],
['email' => 'jane@example.com', 'name' => 'Jane']
],
'Weekly Newsletter',
'Hello {{name}}, here is your newsletter for {{date}}...'
);

Handling Unsubscribes

Unsubscribe Webhook

When a recipient unsubscribes, EmailEngine sends a webhook notification:

{
"serviceUrl": "https://emailengine.example.com",
"account": "sender@example.com",
"date": "2024-10-13T14:23:45.678Z",
"event": "listUnsubscribe",
"data": {
"recipient": "john@example.com",
"messageId": "<a2184d08-a470-fec6-a493-fa211a3756e9@example.com>",
"listId": "newsletter-weekly",
"remoteAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
}

Webhook fields:

FieldDescription
eventAlways listUnsubscribe
data.recipientEmail address that unsubscribed
data.messageIdMessage-ID of the email that triggered the unsubscribe
data.listIdThe list ID from the original email
data.remoteAddressIP address of the unsubscribe request
data.userAgentUser agent string from the unsubscribe request

Webhook Handler Example

// Pseudo code - implement in your preferred language

function handle_webhook(request):
webhook = request.body

if webhook.event == 'listUnsubscribe':
list_id = webhook.data.listId
recipient = webhook.data.recipient
message_id = webhook.data.messageId

PRINT('Unsubscribe: ' + recipient + ' from list ' + list_id + ' (message: ' + message_id + ')')

// Remove from your database
DATABASE_UPDATE(
table='mailing_list',
where={ email: recipient, listId: list_id },
set={ subscribed: false, unsubscribedAt: CURRENT_TIMESTAMP() }
)

// Log unsubscribe
DATABASE_INSERT(
table='unsubscribe_logs',
values={
email: recipient,
listId: list_id,
messageId: message_id,
unsubscribedAt: CURRENT_TIMESTAMP()
}
)

// Send confirmation email (optional)
CALL send_unsubscribe_confirmation(recipient, list_id)

PRINT('Successfully unsubscribed ' + recipient + ' from ' + list_id)
end if

RESPOND(200, { success: true })
end function

function send_unsubscribe_confirmation(email, list_id):
HTTP_POST(
'http://localhost:3000/v1/account/sender@example.com/submit',
headers={
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body=JSON_ENCODE({
to: { address: email },
subject: 'Unsubscribe Confirmation',
text: 'You have been unsubscribed from ' + list_id + '. You will no longer receive these emails.'
})
)
end function

Python Webhook Handler

from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)

@app.route('/webhooks/emailengine', methods=['POST'])
def webhook_handler():
webhook = request.json

if webhook['event'] == 'listUnsubscribe':
data = webhook['data']
list_id = data['listId']
recipient = data['recipient']
message_id = data['messageId']

print(f"Unsubscribe: {recipient} from {list_id} (message: {message_id})")

# Update database
db.mailing_list.update(
{'email': recipient, 'listId': list_id},
{'subscribed': False, 'unsubscribedAt': datetime.now()}
)

# Log unsubscribe
db.unsubscribe_logs.insert({
'email': recipient,
'listId': list_id,
'messageId': message_id,
'unsubscribedAt': datetime.now()
})

print(f"Successfully unsubscribed {recipient}")

return jsonify({'success': True})

if __name__ == '__main__':
app.run(port=3000)

PHP Webhook Handler

<?php
// webhook-handler.php

$webhook = json_decode(file_get_contents('php://input'), true);

if ($webhook['event'] === 'listUnsubscribe') {
$listId = $webhook['data']['listId'];
$recipient = $webhook['data']['recipient'];
$messageId = $webhook['data']['messageId'];

error_log("Unsubscribe: {$recipient} from {$listId} (message: {$messageId})");

// Update database
$stmt = $pdo->prepare('UPDATE mailing_list SET subscribed = 0, unsubscribed_at = NOW() WHERE email = ? AND list_id = ?');
$stmt->execute([$recipient, $listId]);

// Log unsubscribe
$stmt = $pdo->prepare('INSERT INTO unsubscribe_logs (email, list_id, message_id, unsubscribed_at) VALUES (?, ?, ?, NOW())');
$stmt->execute([$recipient, $listId, $messageId]);

error_log("Successfully unsubscribed {$recipient}");
}

echo json_encode(['success' => true]);

List Management

Multiple Lists

Use different listId values for different campaigns:

// Pseudo code - implement in your preferred language

CALL send_email({ listId: 'newsletter-weekly' })
CALL send_email({ listId: 'newsletter-monthly' })
CALL send_email({ listId: 'product-updates' })
CALL send_email({ listId: 'promotional' })

Subscribe Management in Your App

Keep subscription status in your database:

CREATE TABLE mailing_list (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
list_id VARCHAR(100) NOT NULL,
subscribed BOOLEAN DEFAULT true,
subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
unsubscribed_at TIMESTAMP NULL,
UNIQUE(email, list_id)
);

CREATE INDEX idx_subscribed ON mailing_list(email, list_id, subscribed);

Query Subscribers

// Pseudo code - implement in your preferred language

function get_subscribers(list_id):
return DATABASE_FIND(
table='mailing_list',
where={ listId: list_id, subscribed: true }
)
end function

function is_subscribed(email, list_id):
subscriber = DATABASE_FIND_ONE(
table='mailing_list',
where={ email: email, listId: list_id }
)

return subscriber exists AND subscriber.subscribed == true
end function

function unsubscribe(email, list_id):
DATABASE_UPDATE(
table='mailing_list',
where={ email: email, listId: list_id },
set={ subscribed: false, unsubscribedAt: CURRENT_TIMESTAMP() }
)
end function

Prevent Sending to Unsubscribed

Filter unsubscribed recipients before sending:

// Pseudo code - implement in your preferred language

function send_to_list(list_id, subject, content):
// Get all subscribers
all_subscribers = DATABASE_FIND(
table='mailing_list',
where={ listId: list_id }
)

// Filter only subscribed
active_subscribers = FILTER(all_subscribers WHERE subscribed == true)

// Send
CALL send_newsletter(active_subscribers, subject, content)

unsubscribed_count = LENGTH(all_subscribers) - LENGTH(active_subscribers)
PRINT('Sent to ' + LENGTH(active_subscribers) + ' subscribers (' + unsubscribed_count + ' unsubscribed)')
end function

Compliance

CAN-SPAM Act (USA)

RequirementProvided By
Include unsubscribe mechanismVirtual lists
Honor unsubscribes within 10 business daysYou must implement
Include physical postal address in emailsYou must add
Identify message as advertisementYou must add if applicable

GDPR (EU)

RequirementProvided By
Obtain consent before sendingYou must implement
Provide easy unsubscribe methodVirtual lists
Honor unsubscribes immediatelyYou must implement
Keep records of consent and unsubscribesYou must implement

CASL (Canada)

RequirementProvided By
Obtain express consentYou must implement
Include unsubscribe mechanismVirtual lists
Honor unsubscribes within 10 daysYou must implement
Include sender informationYou must add

Limitations

What Virtual Lists Don't Do

Not a full mailing list manager:

  • [NO] No contact storage
  • [NO] No segmentation
  • [NO] No sending history
  • [NO] No bounce management
  • [NO] No analytics/reporting
  • [NO] No A/B testing
  • [NO] No email templates

You must implement:

  • Subscriber database
  • Subscription forms
  • Consent management
  • List segmentation
  • Send scheduling
  • Analytics tracking
  • Bounce handling

When to Use a Full Mailing List Service

Consider a dedicated service (Mailchimp, SendGrid, etc.) if you need:

  • Advanced segmentation
  • Detailed analytics
  • A/B testing
  • Template editors
  • Automation workflows
  • Landing pages
  • Signup forms

EmailEngine virtual lists are best when you already have these features in your app and just need compliant unsubscribe handling.