Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/email-delivery-brevo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Email Deliverability Setup (Brevo & Outlook Junk Prevention)

## Overview
Default Firebase Authentication verification emails lack custom domain authentication and branded styling, causing aggressive filtering algorithms (especially Microsoft Outlook / Exchange / Office 365) to route verification emails to Junk.

## Solution Architecture
1. **Custom SMTP / Transactional Email Provider**: We integrate Brevo (`https://api.brevo.com/v3/smtp/email`) to send HTML branded verification messages with domain signing.
2. **Domain Authentication Requirements**:
- **SPF**: Add `include:spf.brevo.com` to `mobilitydatabase.org` TXT records.
- **DKIM**: Add the DKIM TXT key provided in Brevo Domain Settings (`mail._domainkey.mobilitydatabase.org`).
- **DMARC**: Set a standard DMARC policy (e.g. `v=DMARC1; p=none; rua=mailto:dmarc@mobilitydata.org`).
3. **Environment Variables**:
- `BREVO_API_KEY`: API key from Brevo console.
- `BREVO_SENDER_EMAIL`: Verified sender address (e.g. `no-reply@mobilitydatabase.org`).
4. **Client Guidance**:
- The user verification page informs users to inspect their Junk/Spam folder and explicitly mark messages from `mobilitydatabase.org` as "Not Junk".
5 changes: 5 additions & 0 deletions src/app/[locale]/verify-email/PostRegistration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export default function PostRegistration(): React.ReactElement {
account registration. Please also check your junk or spam folder if
you do not see it in your inbox.
</Box>
<Alert severity='info' sx={{ mt: 2 }}>
Using Microsoft Outlook or a corporate email? Verification emails may
land in your Junk folder. If found there, please mark the message as
&quot;Not Junk&quot; to ensure future notifications arrive safely.
</Alert>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
<Button
variant='contained'
Expand Down
99 changes: 99 additions & 0 deletions src/app/services/email-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Email Service for transactional emails (verification, notifications).
* Integrates with external email providers (such as Brevo / Sendinblue)
* to ensure high domain deliverability across corporate providers (Outlook/Office 365).
*/

export interface SendVerificationEmailPayload {
email: string;
displayName?: string;
verificationLink: string;
}

/**
* Sends a branded email verification message via external transactional email provider (Brevo API).
*/
export async function sendBrandedVerificationEmail(
payload: SendVerificationEmailPayload,
): Promise<{ success: boolean; error?: string }> {
const apiKey = process.env.BREVO_API_KEY;
const senderEmail =
process.env.BREVO_SENDER_EMAIL ?? 'no-reply@mobilitydatabase.org';
const senderName = 'Mobility Database';

if (!apiKey) {
console.warn(
'BREVO_API_KEY not configured. Transactional email was not sent externally.',
);
return { success: false, error: 'BREVO_API_KEY is not configured.' };
}

try {
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Verify your Mobility Database email</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f4f5f7; margin: 0; padding: 24px; color: #172b4d; }
.container { max-width: 580px; margin: 0 auto; background: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header { background-color: #003366; padding: 24px; text-align: center; }
.header h1 { color: #ffffff; margin: 0; font-size: 22px; }
.content { padding: 32px 24px; line-height: 1.6; }
.button { display: inline-block; background-color: #0066cc; color: #ffffff !important; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; margin: 24px 0; }
.footer { background: #f9fafb; padding: 16px 24px; font-size: 12px; color: #6b778c; text-align: center; border-top: 1px solid #ebecf0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Mobility Database</h1>
</div>
<div class="content">
<p>Hello${payload.displayName ? ` ${payload.displayName}` : ''},</p>
<p>Thank you for signing up for the Mobility Database. Please confirm your email address by clicking the button below:</p>
<p style="text-align: center;">
<a href="${payload.verificationLink}" class="button" target="_blank" rel="noopener noreferrer">Verify Email Address</a>
</p>
<p>If the button doesn't work, you can copy and paste this link into your browser:</p>
<p style="word-break: break-all; font-size: 13px; color: #0066cc;">${payload.verificationLink}</p>
<p>If you did not request this email, no action is needed.</p>
</div>
<div class="footer">
&copy; ${new Date().getFullYear()} MobilityData. All rights reserved.<br />
If you are using Outlook or Office 365, please mark this message as "Not Junk" to ensure future delivery.
</div>
</div>
</body>
</html>
`;

const res = await fetch('https://api.brevo.com/v3/smtp/email', {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
'api-key': apiKey,
},
body: JSON.stringify({
sender: { name: senderName, email: senderEmail },
to: [{ email: payload.email, name: payload.displayName || payload.email }],
subject: 'Verify your Mobility Database email address',
htmlContent,
}),
});

if (!res.ok) {
const errText = await res.text();
return { success: false, error: `Brevo API error: ${errText}` };
}

return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown email error',
};
}
}