Routing by Recipient
The most common routing pattern is based on the recipient email address:app.post('/webhooks/inbound', async (req, res) => {
const { to, from, subject, text, html } = req.body.data;
const recipient = to[0].toLowerCase();
// Route based on recipient address
if (recipient.startsWith('support@')) {
await createSupportTicket({ from, subject, body: text || html });
} else if (recipient.startsWith('sales@')) {
await notifySalesTeam({ from, subject, body: text || html });
} else if (recipient.startsWith('feedback@')) {
await storeFeedback({ from, subject, body: text || html });
} else if (recipient.startsWith('billing@')) {
await routeToBilling({ from, subject, body: text || html });
} else {
await handleUnknownRecipient({ recipient, from, subject });
}
res.sendStatus(200);
});
Variable Address Routing
Use variable addresses (plus addressing) to encode data in the email address:app.post('/webhooks/inbound', async (req, res) => {
const { to, from, subject, text } = req.body.data;
const recipient = to[0];
// Parse variable address: reply+{ticketId}@mail.example.com
const match = recipient.match(/^reply\+([^@]+)@/);
if (match) {
const ticketId = match[1];
await addReplyToTicket(ticketId, { from, subject, body: text });
}
res.sendStatus(200);
});
Common Variable Address Patterns
| Pattern | Use Case | Example |
|---|---|---|
reply+{ticketId}@ | Support ticket replies | reply+TKT-1234@mail.example.com |
unsubscribe+{userId}@ | Email unsubscriptions | unsubscribe+usr_abc@mail.example.com |
confirm+{token}@ | Email confirmations | confirm+xyz789@mail.example.com |
order+{orderId}@ | Order communications | order+ORD-5678@mail.example.com |
notify+{channelId}@ | Channel notifications | notify+ch_slack@mail.example.com |
Routing by Subject
Route emails based on subject line keywords or patterns:app.post('/webhooks/inbound', async (req, res) => {
const { from, subject, text } = req.body.data;
const subjectLower = subject.toLowerCase();
if (subjectLower.includes('urgent') || subjectLower.includes('emergency')) {
await createHighPriorityTicket({ from, subject, body: text });
} else if (subjectLower.includes('unsubscribe')) {
await processUnsubscribeRequest({ from });
} else if (subjectLower.match(/re:\s*ticket #\d+/i)) {
const ticketNum = subjectLower.match(/ticket #(\d+)/i)[1];
await addReplyToTicket(ticketNum, { from, body: text });
} else {
await createStandardTicket({ from, subject, body: text });
}
res.sendStatus(200);
});
Routing by Sender Domain
Route based on the sender’s domain for B2B workflows:app.post('/webhooks/inbound', async (req, res) => {
const { from, subject, text } = req.body.data;
const senderDomain = from.split('@')[1].toLowerCase();
// Check if sender is from a known customer domain
const customer = await findCustomerByDomain(senderDomain);
if (customer) {
await routeToAccountManager(customer.accountManagerId, {
from,
subject,
body: text,
customerId: customer.id
});
} else {
await routeToGeneralInbox({ from, subject, body: text });
}
res.sendStatus(200);
});
Content-Based Routing
Route based on email content analysis:app.post('/webhooks/inbound', async (req, res) => {
const { from, subject, text, html } = req.body.data;
const content = text || stripHtml(html);
const contentLower = content.toLowerCase();
// Detect intent from content
if (contentLower.includes('cancel') && contentLower.includes('subscription')) {
await routeToCancellations({ from, subject, body: content });
} else if (contentLower.includes('refund') || contentLower.includes('money back')) {
await routeToRefunds({ from, subject, body: content });
} else if (contentLower.includes('bug') || contentLower.includes('error')) {
await routeToTechnicalSupport({ from, subject, body: content });
} else {
await routeToGeneralSupport({ from, subject, body: content });
}
res.sendStatus(200);
});
Multi-Criteria Routing
Combine multiple routing criteria for complex workflows:app.post('/webhooks/inbound', async (req, res) => {
const { to, from, subject, text, html, spamScore } = req.body.data;
const recipient = to[0].toLowerCase();
const content = text || html;
// Define routing rules
const rules = [
{
name: 'spam',
condition: () => spamScore > 5,
action: async () => await quarantineAsSpam({ from, subject })
},
{
name: 'vip',
condition: () => isVipCustomer(from),
action: async () => await routeToVipSupport({ from, subject, body: content })
},
{
name: 'ticket-reply',
condition: () => recipient.match(/^reply\+/),
action: async () => {
const ticketId = recipient.match(/^reply\+([^@]+)@/)[1];
await addReplyToTicket(ticketId, { from, body: content });
}
},
{
name: 'sales',
condition: () => recipient.startsWith('sales@'),
action: async () => await notifySalesTeam({ from, subject, body: content })
},
{
name: 'default',
condition: () => true,
action: async () => await createSupportTicket({ from, subject, body: content })
}
];
// Execute first matching rule
for (const rule of rules) {
if (rule.condition()) {
await rule.action();
break;
}
}
res.sendStatus(200);
});
Routing with Priority Queues
For high-volume applications, use priority queues:import Queue from 'bull';
const highPriorityQueue = new Queue('email-high', redisUrl);
const normalPriorityQueue = new Queue('email-normal', redisUrl);
const lowPriorityQueue = new Queue('email-low', redisUrl);
app.post('/webhooks/inbound', async (req, res) => {
const { from, subject, text, spamScore } = req.body.data;
// Determine priority
let queue;
if (isVipCustomer(from) || subject.toLowerCase().includes('urgent')) {
queue = highPriorityQueue;
} else if (spamScore > 3) {
queue = lowPriorityQueue;
} else {
queue = normalPriorityQueue;
}
// Add to appropriate queue
await queue.add({
from,
subject,
body: text,
receivedAt: new Date().toISOString()
});
res.sendStatus(200);
});
// Process queues with different concurrency
highPriorityQueue.process(10, processEmail);
normalPriorityQueue.process(5, processEmail);
lowPriorityQueue.process(2, processEmail);
Catch-All Addresses
Handle any address on your inbound domain:app.post('/webhooks/inbound', async (req, res) => {
const { to, from, subject, text } = req.body.data;
const recipient = to[0];
const localPart = recipient.split('@')[0].toLowerCase();
// Known addresses
const knownAddresses = ['support', 'sales', 'feedback', 'billing'];
if (knownAddresses.includes(localPart)) {
await routeToKnownHandler(localPart, { from, subject, body: text });
} else {
// Catch-all: log and potentially notify admin
await logUnknownAddress(recipient, { from, subject });
await notifyAdmin(`Unknown address: ${recipient}`);
}
res.sendStatus(200);
});
Catch-all addresses can receive a lot of spam. Consider implementing spam filtering before processing catch-all emails.
Forwarding Emails
Forward received emails to another address:app.post('/webhooks/inbound', async (req, res) => {
const { from, fromName, subject, text, html, attachments } = req.body.data;
// Forward to internal team
await lettr.emails.send({
from: 'forwarded@yourdomain.com',
to: ['team@yourdomain.com'],
subject: `[Forwarded] ${subject}`,
html: `
<p><strong>Originally from:</strong> ${fromName} <${from}></p>
<hr>
${html || `<pre>${text}</pre>`}
`,
attachments: attachments.map(att => ({
filename: att.filename,
url: att.url
}))
});
res.sendStatus(200);
});
Error Handling
Implement proper error handling in your routing logic:app.post('/webhooks/inbound', async (req, res) => {
try {
const { to, from, subject, text } = req.body.data;
const recipient = to[0];
await routeEmail(recipient, { from, subject, body: text });
res.sendStatus(200);
} catch (error) {
console.error('Routing error:', error);
// Store failed email for manual review
await storeFailedEmail(req.body.data, error.message);
// Return 200 to prevent retries if it's a processing error
// Return 500 to trigger retry if it's a temporary error
if (error.isTemporary) {
res.sendStatus(500);
} else {
res.sendStatus(200);
}
}
});
Related Topics
Webhooks
Set up webhook endpoints
Reply Tracking
Track and thread email replies
Spam Filtering
Filter spam from inbound emails
Best Practices
Inbound best practices