Система управления отписками от рассылок
Корректное управление отписками — это не только вежливость, но и юридическое требование (GDPR, CAN-SPAM, российский ФЗ-152). Нарушения ведут к блокировке домена и штрафам. Нужна надёжная система: одноклик-отписка, управление предпочтениями и уважение к выбору пользователя.
Таблица подписок и предпочтений
CREATE TABLE email_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
subscription_type VARCHAR(100) NOT NULL,
-- 'marketing', 'digest', 'product_updates', 'security', 'transactional'
is_active BOOLEAN NOT NULL DEFAULT true,
unsubscribed_at TIMESTAMPTZ,
unsubscribe_reason TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (user_id, subscription_type)
);
CREATE TABLE email_suppression_list (
email VARCHAR(255) PRIMARY KEY,
reason VARCHAR(100) NOT NULL, -- 'unsubscribe', 'bounce', 'complaint'
created_at TIMESTAMPTZ DEFAULT now()
);
Генерация подписанной ссылки отписки
import { createHmac } from 'crypto';
function generateUnsubscribeToken(userId: string, type: string): string {
const payload = `${userId}:${type}:${Date.now()}`;
const signature = createHmac('sha256', process.env.UNSUBSCRIBE_SECRET!)
.update(payload)
.digest('hex');
return Buffer.from(`${payload}:${signature}`).toString('base64url');
}
function generateUnsubscribeUrl(userId: string, type: string = 'all'): string {
const token = generateUnsubscribeToken(userId, type);
return `https://app.example.com/unsubscribe/${token}`;
}
Обработчик отписки
// GET /unsubscribe/:token — ссылка в письме (one-click preview)
app.get('/unsubscribe/:token', async (req, res) => {
const decoded = parseUnsubscribeToken(req.params.token);
if (!decoded) return res.status(400).render('unsubscribe-invalid');
// Показать страницу подтверждения / управления предпочтениями
res.render('unsubscribe', {
userId: decoded.userId,
type: decoded.type,
token: req.params.token,
});
});
// POST /unsubscribe/:token — подтверждение отписки
app.post('/unsubscribe/:token', async (req, res) => {
const decoded = parseUnsubscribeToken(req.params.token);
if (!decoded) return res.status(400).json({ error: 'Invalid token' });
const { type, reason } = req.body;
if (type === 'all') {
// Отписать от всех маркетинговых рассылок
await db.query(
`UPDATE email_subscriptions
SET is_active = false, unsubscribed_at = now(), unsubscribe_reason = $1
WHERE user_id = $2 AND subscription_type != 'transactional'`,
[reason, decoded.userId]
);
// Добавить в список подавления
const user = await db.users.findById(decoded.userId);
await db.query(
`INSERT INTO email_suppression_list (email, reason) VALUES ($1, 'unsubscribe')
ON CONFLICT (email) DO NOTHING`,
[user.email]
);
} else {
// Отписать от конкретного типа
await db.query(
`UPDATE email_subscriptions
SET is_active = false, unsubscribed_at = now()
WHERE user_id = $1 AND subscription_type = $2`,
[decoded.userId, type]
);
}
res.json({ ok: true });
});
List-Unsubscribe заголовок (RFC 8058)
Gmail и Outlook показывают кнопку «Отписаться» в интерфейсе, если письмо содержит заголовок List-Unsubscribe. Это обязательно для отправителей > 5 000 писем в день с февраля 2024:
await sendEmail({
to: user.email,
subject: 'Наш дайджест',
html: emailHtml,
headers: {
// RFC 2369 — mailto для почтовых клиентов
'List-Unsubscribe': `<mailto:[email protected]?subject=unsub-${userId}>, <https://app.example.com/unsubscribe/${token}>`,
// RFC 8058 — POST-запрос без открытия браузера (Gmail one-click)
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
},
});
// POST /api/email/list-unsubscribe — обработчик One-Click
app.post('/api/email/list-unsubscribe', async (req, res) => {
const { 'list-unsubscribe' }: { 'list-unsubscribe': string } = req.body;
// Поле = 'One-Click' — просто обработать
// Идентификация пользователя — через X-Token header или token в URL
res.status(200).end();
});
Страница управления предпочтениями
Вместо полной отписки — предложить выбрать типы рассылок:
| Тип | Описание | Отключаемый |
|---|---|---|
transactional |
Подтверждения, инвойсы | Нет |
security |
Вход с нового устройства | Нет |
product_updates |
Обновления продукта | Да |
marketing |
Промо и скидки | Да |
digest |
Еженедельный дайджест | Да |
Сроки
Система отписок с одноклик-ссылкой, List-Unsubscribe заголовком, страницей управления предпочтениями — 2–3 дня.







