Skip to content

Commit 71c57d1

Browse files
feat: add email verification based on spf/dkim/dmarc/logo verification (#1622)
Co-authored-by: Ahmet Kilinc <akx9@icloud.com>
1 parent e30e7c9 commit 71c57d1

File tree

7 files changed

+2222
-14
lines changed

7 files changed

+2222
-14
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
2+
import { useTRPC } from '@/providers/query-provider';
3+
import { useQuery } from '@tanstack/react-query';
4+
import { CircleCheck } from '../icons/icons';
5+
import React from 'react';
6+
7+
interface EmailVerificationBadgeProps {
8+
messageId: string | undefined;
9+
}
10+
11+
export const EmailVerificationBadge: React.FC<EmailVerificationBadgeProps> = ({ messageId }) => {
12+
const trpc = useTRPC();
13+
14+
const {
15+
data: verificationResult,
16+
isLoading,
17+
isError,
18+
} = useQuery({
19+
...trpc.mail.verifyEmail.queryOptions({ id: messageId || '' }),
20+
enabled: !!messageId,
21+
staleTime: 5 * 60 * 1000,
22+
retry: 1,
23+
refetchOnWindowFocus: false,
24+
refetchOnReconnect: false,
25+
});
26+
27+
if (!verificationResult?.isVerified || isLoading || isError) {
28+
return null;
29+
}
30+
31+
return (
32+
<Tooltip>
33+
<TooltipTrigger asChild>
34+
<div className="flex items-center">
35+
<CircleCheck className="h-4 w-4 fill-blue-600 text-blue-600 dark:fill-blue-500 dark:text-blue-500" />
36+
</div>
37+
</TooltipTrigger>
38+
<TooltipContent>
39+
<p className="text-sm">
40+
Verified sender - This email passed email authentication (SPF/DKIM/DMARC) and BIMI
41+
validation
42+
</p>
43+
</TooltipContent>
44+
</Tooltip>
45+
);
46+
};

apps/mail/components/mail/mail-display.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { Dialog, DialogTitle, DialogHeader, DialogContent } from '../ui/dialog';
3535
import { memo, useEffect, useMemo, useState, useRef, useCallback } from 'react';
3636
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
3737
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
38+
import { EmailVerificationBadge } from './email-verification-badge';
3839
import type { Sender, ParsedMessage, Attachment } from '@/types';
3940
import { useActiveConnection } from '@/hooks/use-connections';
4041
import { useAttachments } from '@/hooks/use-attachments';
@@ -1327,20 +1328,23 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
13271328
<div className="flex w-full flex-col">
13281329
<div className="flex w-full items-center justify-between">
13291330
<div className="flex items-center gap-1">
1330-
<span
1331-
onClick={(e) => {
1332-
e.stopPropagation();
1333-
e.preventDefault();
1334-
setResearchSender({
1335-
name: emailData?.sender?.name || '',
1336-
email: emailData?.sender?.email || '',
1337-
// extra: emailData?.sender?.extra || '',
1338-
});
1339-
}}
1340-
className="hover:bg-muted max-w-36 truncate whitespace-nowrap font-semibold md:max-w-none"
1341-
>
1342-
{cleanNameDisplay(emailData?.sender?.name)}
1343-
</span>
1331+
<div className="flex items-center gap-2">
1332+
<span
1333+
onClick={(e) => {
1334+
e.stopPropagation();
1335+
e.preventDefault();
1336+
setResearchSender({
1337+
name: emailData?.sender?.name || '',
1338+
email: emailData?.sender?.email || '',
1339+
// extra: emailData?.sender?.extra || '',
1340+
});
1341+
}}
1342+
className="hover:bg-muted font-semibold"
1343+
>
1344+
{cleanNameDisplay(emailData?.sender?.name)}
1345+
</span>
1346+
<EmailVerificationBadge messageId={emailData?.id} />
1347+
</div>
13441348

13451349
<Popover open={openDetailsPopover} onOpenChange={handlePopoverChange}>
13461350
<PopoverTrigger asChild>

apps/server/src/lib/driver/google.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,28 @@ export class GoogleMailManager implements MailManager {
913913
);
914914
}
915915

916+
public getRawEmail(messageId: string) {
917+
return this.withErrorHandler(
918+
'getRawEmail',
919+
async () => {
920+
const res = await this.gmail.users.messages.get({
921+
userId: 'me',
922+
id: messageId,
923+
format: 'raw',
924+
quotaUser: this.config.auth?.email,
925+
});
926+
927+
if (!res.data.raw) {
928+
throw new Error('No raw email data found');
929+
}
930+
931+
const rawEmail = Buffer.from(res.data.raw, 'base64').toString('utf-8');
932+
return rawEmail;
933+
},
934+
{ messageId, email: this.config.auth?.email },
935+
);
936+
}
937+
916938
private async getThreadMetadata(threadId: string) {
917939
return this.withErrorHandler(
918940
'getThreadMetadata',

apps/server/src/lib/driver/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export interface MailManager {
116116
getEmailAliases(): Promise<{ email: string; name?: string; primary?: boolean }[]>;
117117
revokeToken(token: string): Promise<boolean>;
118118
deleteAllSpam(): Promise<DeleteAllSpamResponse>;
119+
getRawEmail(id: string): Promise<string>;
119120
}
120121

121122
export interface IGetThreadsResponse {

0 commit comments

Comments
 (0)