Merge pull request #885 from Mail-0/feat/outlook-provider

adds outlook provider
This commit is contained in:
Adam
2025-05-19 14:58:43 -07:00
committed by GitHub
17 changed files with 1675 additions and 862 deletions

View File

@@ -4,7 +4,7 @@ import { useEffect, type ReactNode, useState, Suspense } from 'react';
import type { EnvVarInfo } from '@zero/server/auth-providers';
import ErrorMessage from '@/app/(auth)/login/error-message';
import { signIn, useSession } from '@/lib/auth-client';
import { Google } from '@/components/icons/icons';
import { Google, Microsoft } from '@/components/icons/icons';
import { Button } from '@/components/ui/button';
import { TriangleAlert } from 'lucide-react';
import { useRouter } from 'next/navigation';
@@ -42,6 +42,9 @@ const getProviderIcon = (providerId: string, className?: string): ReactNode => {
case 'google':
return <Google className={defaultClass} />;
case 'microsoft':
return <Microsoft className={defaultClass} />;
case 'zero':
return (
<>

View File

@@ -24,7 +24,6 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { env } from '@/lib/env';
import Image from 'next/image';
import { toast } from 'sonner';
@@ -80,119 +79,122 @@ export default function ConnectionsPage() {
</div>
) : data?.connections?.length ? (
<div className="lg: grid gap-4 sm:grid-cols-1 md:grid-cols-2">
{data.connections.map((connection) => (
<div
key={connection.id}
className="bg-popover flex items-center justify-between rounded-lg border p-4"
>
<div className="flex min-w-0 items-center gap-4">
{connection.picture ? (
<Image
src={connection.picture}
alt={connection.id + ' profile picture'}
className="h-12 w-12 shrink-0 rounded-lg object-cover"
width={48}
height={48}
/>
) : (
<div className="bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-lg">
<svg viewBox="0 0 24 24" className="text-primary h-6 w-6">
<path fill="currentColor" d={emailProviders[0]!.icon} />
</svg>
</div>
)}
<div className="flex min-w-0 flex-col gap-1">
<span className="truncate text-sm font-medium">{connection.name}</span>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<Tooltip
delayDuration={0}
open={openTooltip === connection.id}
onOpenChange={(open) => {
if (window.innerWidth <= 768) {
setOpenTooltip(open ? connection.id : null);
}
}}
>
<TooltipTrigger asChild>
<span
className="max-w-[180px] cursor-default truncate sm:max-w-[240px] md:max-w-[300px]"
onClick={() => {
if (window.innerWidth <= 768) {
setOpenTooltip(
openTooltip === connection.id ? null : connection.id,
);
}
}}
>
{connection.email}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="select-all">
<div className="font-mono">{connection.email}</div>
</TooltipContent>
</Tooltip>
{data.connections.map((connection) => {
const Icon = emailProviders.find(
(p) => p.providerId === connection.providerId,
)?.icon;
return (
<div
key={connection.id}
className="bg-popover flex items-center justify-between rounded-lg border p-4"
>
<div className="flex min-w-0 items-center gap-4">
{connection.picture ? (
<Image
src={connection.picture}
alt=""
className="h-12 w-12 shrink-0 rounded-lg object-cover"
width={48}
height={48}
/>
) : (
<div className="bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-lg">
{Icon && <Icon className="size-6" />}
</div>
)}
<div className="flex min-w-0 flex-col gap-1">
<span className="truncate text-sm font-medium">{connection.name}</span>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<Tooltip
delayDuration={0}
open={openTooltip === connection.id}
onOpenChange={(open) => {
if (window.innerWidth <= 768) {
setOpenTooltip(open ? connection.id : null);
}
}}
>
<TooltipTrigger asChild>
<span
className="max-w-[180px] cursor-default truncate sm:max-w-[240px] md:max-w-[300px]"
onClick={() => {
if (window.innerWidth <= 768) {
setOpenTooltip(
openTooltip === connection.id ? null : connection.id,
);
}
}}
>
{connection.email}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="select-all">
<div className="font-mono">{connection.email}</div>
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<div className="flex items-center gap-4">
{data.disconnectedIds?.includes(connection.id) ? (
<>
<div>
<Badge variant="destructive">
{t('pages.settings.connections.disconnected')}
</Badge>
</div>
<Button
variant="secondary"
size="sm"
onClick={async () => {
await authClient.linkSocial({
provider: connection.providerId,
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/settings/connections`,
});
}}
>
<Unplug className="size-4" />
{t('pages.settings.connections.reconnect')}
</Button>
</>
) : null}
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
>
<Trash className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent showOverlay>
<DialogHeader>
<DialogTitle>
{t('pages.settings.connections.disconnectTitle')}
</DialogTitle>
<DialogDescription>
{t('pages.settings.connections.disconnectDescription')}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-4">
<DialogClose asChild>
<Button variant="outline">
{t('pages.settings.connections.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={() => disconnectAccount(connection.id)}>
{t('pages.settings.connections.remove')}
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
</div>
<div className="flex items-center gap-4">
{data.disconnectedIds?.includes(connection.id) ? (
<>
<div>
<Badge variant="destructive">
{t('pages.settings.connections.disconnected')}
</Badge>
</div>
<Button
variant="secondary"
size="sm"
onClick={async () => {
await authClient.linkSocial({
provider: connection.providerId,
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/settings/connections`,
});
}}
>
<Unplug className="size-4" />
{t('pages.settings.connections.reconnect')}
</Button>
</>
) : null}
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
>
<Trash className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('pages.settings.connections.disconnectTitle')}
</DialogTitle>
<DialogDescription>
{t('pages.settings.connections.disconnectDescription')}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-4">
<DialogClose asChild>
<Button variant="outline">
{t('pages.settings.connections.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={() => disconnectAccount(connection.id)}>
{t('pages.settings.connections.remove')}
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
</div>
))}
);
})}
</div>
) : null}

View File

@@ -15,7 +15,6 @@ import { useTranslations } from 'next-intl';
import { Button } from '../ui/button';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
import { env } from '@/lib/env';
import { useMemo } from 'react';
import { toast } from 'sonner';
@@ -31,11 +30,11 @@ export const AddConnectionDialog = ({
const { connections, attach } = useBilling();
const t = useTranslations();
const pathname = usePathname();
const canCreateConnection = useMemo(() => {
if (!connections?.remaining && !connections?.unlimited) return false;
return (connections?.unlimited && !connections?.remaining) || (connections?.remaining ?? 0) > 0;
}, [connections]);
const pathname = usePathname();
const handleUpgrade = async () => {
if (attach) {
@@ -97,33 +96,34 @@ export const AddConnectionDialog = ({
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{emailProviders.map((provider, index) => (
<motion.div
key={provider.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.3 }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
<Button
disabled={!canCreateConnection}
variant="outline"
className="h-24 w-full flex-col items-center justify-center gap-2"
onClick={async () =>
await authClient.linkSocial({
provider: provider.providerId,
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/${pathname}`,
})
}
{emailProviders.map((provider, index) => {
const Icon = provider.icon;
return (
<motion.div
key={provider.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.3 }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
<svg viewBox="0 0 24 24" className="h-12 w-12">
<path fill="currentColor" d={provider.icon} />
</svg>
<span className="text-xs">{provider.name}</span>
</Button>
</motion.div>
))}
<Button
disabled={!canCreateConnection}
variant="outline"
className="h-24 w-full flex-col items-center justify-center gap-2"
onClick={async () =>
await authClient.linkSocial({
provider: provider.providerId,
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/${pathname}`,
})
}
>
<Icon className="!size-6" />
<span className="text-xs">{provider.name}</span>
</Button>
</motion.div>
);
})}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}

View File

@@ -15,6 +15,26 @@ export const Gmail = ({ className }: { className?: string }) => (
</svg>
);
export const GmailColor = ({ className }: { className?: string }) => (
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="52 42 88 66" className={className}>
<path fill="#4285f4" d="M58 108h14V74L52 59v43c0 3.32 2.69 6 6 6" />
<path fill="#34a853" d="M120 108h14c3.32 0 6-2.69 6-6V59l-20 15" />
<path fill="#fbbc04" d="M120 48v26l20-15v-8c0-7.42-8.47-11.65-14.4-7.2" />
<path fill="#ea4335" d="M72 74V48l24 18 24-18v26L96 92" />
<path fill="#c5221f" d="M52 51v8l20 15V48l-5.6-4.2c-5.94-4.45-14.4-.22-14.4 7.2" />
</svg>
);
export const Microsoft = ({ className }: { className?: string }) => (
<svg role="img" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg" className={className}>
<title>Microsoft</title>
<path
d="M0 32h214.6v214.6H0V32zm233.4 0H448v214.6H233.4V32zM0 265.4h214.6V480H0V265.4zm233.4 0H448V480H233.4V265.4z"
fill="var(--icon-color)"
/>
</svg>
);
export const Outlook = ({ className }: { className?: string }) => (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className}>
<title>Outlook</title>
@@ -25,6 +45,108 @@ export const Outlook = ({ className }: { className?: string }) => (
</svg>
);
export const OutlookColor = ({ className }: { className?: string }) => (
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1831.085 1703.335"
className={className}
>
<path
fill="#0A2767"
d="M1831.083,894.25c0.1-14.318-7.298-27.644-19.503-35.131h-0.213l-0.767-0.426l-634.492-375.585 c-2.74-1.851-5.583-3.543-8.517-5.067c-24.498-12.639-53.599-12.639-78.098,0c-2.934,1.525-5.777,3.216-8.517,5.067L446.486,858.693 l-0.766,0.426c-19.392,12.059-25.337,37.556-13.278,56.948c3.553,5.714,8.447,10.474,14.257,13.868l634.492,375.585 c2.749,1.835,5.592,3.527,8.517,5.068c24.498,12.639,53.599,12.639,78.098,0c2.925-1.541,5.767-3.232,8.517-5.068l634.492-375.585 C1823.49,922.545,1831.228,908.923,1831.083,894.25z"
/>
<path
fill="#0364B8"
d="M520.453,643.477h416.38v381.674h-416.38V643.477z M1745.917,255.5V80.908 c1-43.652-33.552-79.862-77.203-80.908H588.204C544.552,1.046,510,37.256,511,80.908V255.5l638.75,170.333L1745.917,255.5z"
/>
<path fill="#0078D4" d="M511,255.5h425.833v383.25H511V255.5z" />
<path
fill="#28A8EA"
d="M1362.667,255.5H936.833v383.25L1362.667,1022h383.25V638.75L1362.667,255.5z"
/>
<path fill="#0078D4" d="M936.833,638.75h425.833V1022H936.833V638.75z" />
<path fill="#0364B8" d="M936.833,1022h425.833v383.25H936.833V1022z" />
<path fill="#14447D" d="M520.453,1025.151h416.38v346.969h-416.38V1025.151z" />
<path fill="#0078D4" d="M1362.667,1022h383.25v383.25h-383.25V1022z" />
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="1128.4584"
y1="811.0833"
x2="1128.4584"
y2="1.9982"
gradientTransform="matrix(1 0 0 -1 0 1705.3334)"
>
<stop offset="0" style={{ stopColor: '#35B8F1' }} />
<stop offset="1" style={{ stopColor: '#28A8EA' }} />
</linearGradient>
<path
fill="url(#SVGID_1_)"
d="M1811.58,927.593l-0.809,0.426l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-10.777,5.132-22.481,8.029-34.407,8.517l-34.663-20.27c-2.929-1.47-5.773-3.105-8.517-4.897L447.167,906.003h-0.298 l-21.036-11.753v722.384c0.328,48.196,39.653,87.006,87.849,86.7h1230.914c0.724,0,1.363-0.341,2.129-0.341 c10.18-0.651,20.216-2.745,29.808-6.217c4.145-1.756,8.146-3.835,11.966-6.217c2.853-1.618,7.75-5.152,7.75-5.152 c21.814-16.142,34.726-41.635,34.833-68.772V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z"
/>
<path
opacity="0.5"
fill="#0A2767"
d="M1797.017,891.397v44.287l-663.448,456.791L446.699,906.301 c0-0.235-0.191-0.426-0.426-0.426l0,0l-63.023-37.899v-31.938l25.976-0.426l54.932,31.512l1.277,0.426l4.684,2.981 c0,0,645.563,368.346,647.267,369.197l24.698,14.478c2.129-0.852,4.258-1.703,6.813-2.555 c1.278-0.852,640.879-360.681,640.879-360.681L1797.017,891.397z"
/>
<path
fill="#1490DF"
d="M1811.58,927.593l-0.809,0.468l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-24.641,12.038-53.457,12.038-78.098,0c-2.918-1.445-5.76-3.037-8.517-4.769L446.657,928.061l-0.766-0.468 c-12.25-6.642-19.93-19.409-20.057-33.343v722.384c0.305,48.188,39.616,87.004,87.803,86.7c0.001,0,0.002,0,0.004,0h1229.636 c48.188,0.307,87.5-38.509,87.807-86.696c0-0.001,0-0.002,0-0.004V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z"
/>
<path
opacity="0.1"
d="M1185.52,1279.629l-9.496,5.323c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l241.405,285.479l421.107,101.476c11.539-8.716,20.717-20.178,26.7-33.343L1185.52,1279.629 z"
/>
<path
opacity="0.05"
d="M1228.529,1255.442l-52.505,29.51c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l113.101,311.838l549.538,74.989c21.649-16.254,34.394-41.743,34.407-68.815v-9.326 L1228.529,1255.442z"
/>
<path
fill="#28A8EA"
d="M514.833,1703.333h1228.316c18.901,0.096,37.335-5.874,52.59-17.033l-697.089-408.331 c-2.929-1.47-5.773-3.105-8.517-4.897L447.125,906.088h-0.298l-20.993-11.838v719.914 C425.786,1663.364,465.632,1703.286,514.833,1703.333C514.832,1703.333,514.832,1703.333,514.833,1703.333z"
/>
<path
opacity="0.1"
d="M1022,418.722v908.303c-0.076,31.846-19.44,60.471-48.971,72.392 c-9.148,3.931-19,5.96-28.957,5.962H425.833V383.25H511v-42.583h433.073C987.092,340.83,1021.907,375.702,1022,418.722z"
/>
<path
opacity="0.2"
d="M979.417,461.305v908.302c0.107,10.287-2.074,20.469-6.388,29.808 c-11.826,29.149-40.083,48.273-71.54,48.417H425.833V383.25h475.656c12.356-0.124,24.533,2.958,35.344,8.943 C962.937,405.344,979.407,432.076,979.417,461.305z"
/>
<path
opacity="0.2"
d="M979.417,461.305v823.136c-0.208,43-34.928,77.853-77.927,78.225H425.833V383.25 h475.656c12.356-0.124,24.533,2.958,35.344,8.943C962.937,405.344,979.407,432.076,979.417,461.305z"
/>
<path
opacity="0.2"
d="M936.833,461.305v823.136c-0.046,43.067-34.861,78.015-77.927,78.225H425.833 V383.25h433.072c43.062,0.023,77.951,34.951,77.927,78.013C936.833,461.277,936.833,461.291,936.833,461.305z"
/>
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="162.7469"
y1="1383.0741"
x2="774.0864"
y2="324.2592"
gradientTransform="matrix(1 0 0 -1 0 1705.3334)"
>
<stop offset="0" style={{ stopColor: '#1784D9' }} />
<stop offset="0.5" style={{ stopColor: '#107AD5' }} />
<stop offset="1" style={{ stopColor: '#0A63C9' }} />
</linearGradient>
<path
fill="url(#SVGID_2_)"
d="M78.055,383.25h780.723c43.109,0,78.055,34.947,78.055,78.055v780.723 c0,43.109-34.946,78.055-78.055,78.055H78.055c-43.109,0-78.055-34.947-78.055-78.055V461.305 C0,418.197,34.947,383.25,78.055,383.25z"
/>
<path
fill="#FFFFFF"
d="M243.96,710.631c19.238-40.988,50.29-75.289,89.17-98.495c43.057-24.651,92.081-36.94,141.675-35.515 c45.965-0.997,91.321,10.655,131.114,33.683c37.414,22.312,67.547,55.004,86.742,94.109c20.904,43.09,31.322,90.512,30.405,138.396 c1.013,50.043-9.706,99.628-31.299,144.783c-19.652,40.503-50.741,74.36-89.425,97.388c-41.327,23.734-88.367,35.692-136.011,34.578 c-46.947,1.133-93.303-10.651-134.01-34.067c-37.738-22.341-68.249-55.07-87.892-94.28c-21.028-42.467-31.57-89.355-30.745-136.735 C212.808,804.859,223.158,755.686,243.96,710.631z M339.006,941.858c10.257,25.912,27.651,48.385,50.163,64.812 c22.93,16.026,50.387,24.294,78.353,23.591c29.783,1.178,59.14-7.372,83.634-24.358c22.227-16.375,39.164-38.909,48.715-64.812 c10.677-28.928,15.946-59.572,15.543-90.404c0.33-31.127-4.623-62.084-14.649-91.554c-8.855-26.607-25.246-50.069-47.182-67.537 c-23.88-17.79-53.158-26.813-82.91-25.55c-28.572-0.74-56.644,7.593-80.184,23.804c-22.893,16.496-40.617,39.168-51.1,65.365 c-23.255,60.049-23.376,126.595-0.341,186.728L339.006,941.858z"
/>
<path fill="#50D9FF" d="M1362.667,255.5h383.25v383.25h-383.25V255.5z" />
</svg>
);
export const Discord = ({ className }: { className?: string }) => (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className}>
<title>Discord</title>

View File

@@ -35,6 +35,7 @@ import { backgroundQueueAtom } from '@/store/backgroundQueue';
import { handleUnsubscribe } from '@/lib/email-utils.client';
import { useMediaQuery } from '../../hooks/use-media-query';
import { useSearchValue } from '@/hooks/use-search-value';
import { useConnections } from '@/hooks/use-connections';
import { MailList } from '@/components/mail/mail-list';
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useParams, useRouter } from 'next/navigation';
@@ -319,10 +320,18 @@ export function MailLayout() {
const isMobile = useIsMobile();
const router = useRouter();
const { data: session, isPending } = useSession();
const { data: connections } = useConnections();
const t = useTranslations();
const prevFolderRef = useRef(folder);
const { enableScope, disableScope } = useHotkeysContext();
const activeAccount = useMemo(() => {
if (!session?.activeConnection?.id || !connections?.connections) return null;
return connections.connections.find(
(connection) => connection.id === session.activeConnection?.id,
);
}, [session?.activeConnection?.id, connections?.connections]);
useEffect(() => {
if (prevFolderRef.current !== folder && mail.bulkSelected.length > 0) {
clearBulkSelection();
@@ -454,7 +463,7 @@ export function MailLayout() {
<div className="p-2 px-[22px]">
<SearchBar />
<div className="mt-2">
{folder === 'inbox' && (
{activeAccount?.providerId === 'google' && folder === 'inbox' && (
<CategorySelect isMultiSelectMode={mail.bulkSelected.length > 0} />
)}
</div>

View File

@@ -51,8 +51,7 @@ import { toast } from 'sonner';
import Link from 'next/link';
export function NavUser() {
const { data: session, refetch } = useSession();
const router = useRouter();
const { data: session, refetch: refetchSession } = useSession();
const { data, refetch: refetchConnections } = useConnections();
const [isRendered, setIsRendered] = useState(false);
const [showPricing, setShowPricing] = useState(false);
@@ -60,9 +59,6 @@ export function NavUser() {
const t = useTranslations();
const { state } = useSidebar();
const trpc = useTRPC();
const { refetch: refetchStats } = useStats();
const [{ refetch: refetchThreads }] = useThreads();
const { refetch: refetchLabels } = useLabels();
const { mutateAsync: setDefaultConnection } = useMutation(
trpc.connections.setDefault.mutationOptions(),
);
@@ -98,20 +94,13 @@ export function NavUser() {
useEffect(() => setIsRendered(true), []);
const refetchBrainLabels = useCallback(() => {
queryClient.invalidateQueries({ queryKey: trpc.brain.getLabels.queryKey() });
}, [queryClient]);
const handleAccountSwitch = (connectionId: string) => async () => {
if (connectionId === session?.connectionId) return;
await setDefaultConnection({ connectionId });
refetch();
refetchConnections();
refetchThreads();
refetchLabels();
refetchStats();
refetchBrainState();
refetchBrainLabels();
await refetchConnections();
refetchSession();
// TODO: fix this cache issue, for now this is a quick fix to hard refresh the page
window.location.href = pathname;
};
const handleLogout = async () => {

View File

@@ -1,3 +1,4 @@
import { GmailColor, OutlookColor } from '../components/icons/icons';
import { env } from '@/lib/env';
export const I18N_LOCALE_COOKIE_NAME = 'i18n:locale';
@@ -14,15 +15,15 @@ export const CACHE_BURST_KEY = 'cache-burst:v0.0.2';
export const emailProviders = [
{
name: 'Google',
icon: 'M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z',
name: 'Gmail',
icon: GmailColor,
providerId: 'google',
},
// {
// name: 'Microsoft',
// icon: 'M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z',
// providerId: 'microsoft',
// },
{
name: 'Outlook',
icon: OutlookColor,
providerId: 'microsoft',
},
] as const;
interface GmailColor {

View File

@@ -86,7 +86,7 @@
"@zero/server": "workspace:*",
"ai": "^4.3.9",
"autumn-js": "0.0.22",
"better-auth": "1.2.7",
"better-auth": "1.2.8-beta.7",
"canvas-confetti": "1.9.3",
"cheerio": "1.0.0",
"class-variance-authority": "0.7.1",

View File

@@ -16,6 +16,8 @@
"dependencies": {
"@ai-sdk/openai": "^1.3.21",
"@hono/trpc-server": "^0.3.4",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@microsoft/microsoft-graph-types": "^2.40.0",
"@react-email/components": "^0.0.38",
"@react-email/render": "^1.1.0",
"@trpc/client": "^11.1.2",
@@ -32,6 +34,7 @@
"googleapis": "^148.0.0",
"he": "^1.2.0",
"hono": "^4.7.8",
"hono-party": "^0.0.12",
"jsonrepair": "^3.12.0",
"mimetext": "^3.0.27",
"partyserver": "^0.0.71",

View File

@@ -50,25 +50,31 @@ export const authProviders = (env: Record<string, string>): ProviderConfig[] =>
},
required: true,
},
// {
// id: 'microsoft',
// name: 'Microsoft',
// requiredEnvVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'],
// envVarInfo: [
// { name: 'MICROSOFT_CLIENT_ID', source: 'Microsoft Azure App ID' },
// { name: 'MICROSOFT_CLIENT_SECRET', source: 'Microsoft Azure App Password' },
// ],
// config: {
// clientId: env.MICROSOFT_CLIENT_ID!,
// clientSecret: env.MICROSOFT_CLIENT_SECRET!,
// redirectUri: env.MICROSOFT_REDIRECT_URI!,
// scope: ['https://graph.microsoft.com/User.Read', 'offline_access'],
// authority: 'https://login.microsoftonline.com/common',
// responseType: 'code',
// prompt: 'consent',
// loginHint: 'email',
// },
// },
{
id: 'microsoft',
name: 'Microsoft',
requiredEnvVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'],
envVarInfo: [
{ name: 'MICROSOFT_CLIENT_ID', source: 'Microsoft Azure App ID' },
{ name: 'MICROSOFT_CLIENT_SECRET', source: 'Microsoft Azure App Password' },
],
config: {
clientId: env.MICROSOFT_CLIENT_ID,
clientSecret: env.MICROSOFT_CLIENT_SECRET,
redirectUri: env.MICROSOFT_REDIRECT_URI,
scope: [
'https://graph.microsoft.com/User.Read',
'https://graph.microsoft.com/Mail.ReadWrite',
'https://graph.microsoft.com/Mail.Send',
'offline_access',
],
authority: 'https://login.microsoftonline.com/common',
responseType: 'code',
prompt: 'consent',
loginHint: 'email',
disableProfilePhoto: true,
},
},
];
export function isProviderEnabled(provider: ProviderConfig, env: Record<string, string>): boolean {

View File

@@ -31,7 +31,12 @@ const connectionHandlerHook = async (account: Account) => {
}
const driver = createDriver(account.providerId, {
auth: { accessToken: account.accessToken, refreshToken: account.refreshToken, email: '' },
auth: {
accessToken: account.accessToken,
refreshToken: account.refreshToken,
userId: account.userId,
email: '',
},
});
const userInfo = await driver.getUserInfo().catch(() => {

View File

@@ -34,7 +34,6 @@ export class GoogleMailManager implements MailManager {
this.gmail = google.gmail({ version: 'v1', auth: this.auth });
}
public getScope(): string {
return [
'https://www.googleapis.com/auth/gmail.modify',
@@ -42,7 +41,6 @@ export class GoogleMailManager implements MailManager {
'https://www.googleapis.com/auth/userinfo.email',
].join(' ');
}
public getAttachment(messageId: string, attachmentId: string) {
return this.withErrorHandler(
'getAttachment',
@@ -62,7 +60,6 @@ export class GoogleMailManager implements MailManager {
{ messageId, attachmentId },
);
}
public getEmailAliases() {
return this.withErrorHandler('getEmailAliases', async () => {
const profile = await this.gmail.users.getProfile({
@@ -95,7 +92,6 @@ export class GoogleMailManager implements MailManager {
return aliases;
});
}
public markAsRead(threadIds: string[]) {
return this.withErrorHandler(
'markAsRead',
@@ -105,7 +101,6 @@ export class GoogleMailManager implements MailManager {
{ threadIds },
);
}
public markAsUnread(threadIds: string[]) {
return this.withErrorHandler(
'markAsUnread',
@@ -115,7 +110,6 @@ export class GoogleMailManager implements MailManager {
{ threadIds },
);
}
public getUserInfo() {
return this.withErrorHandler(
'getUserInfo',
@@ -132,7 +126,6 @@ export class GoogleMailManager implements MailManager {
{},
);
}
public getTokens<T>(code: string) {
return this.withErrorHandler(
'getTokens',
@@ -143,7 +136,6 @@ export class GoogleMailManager implements MailManager {
{ code },
);
}
public count() {
return this.withErrorHandler(
'count',
@@ -174,7 +166,6 @@ export class GoogleMailManager implements MailManager {
{ email: this.config.auth?.email },
);
}
public list(params: {
folder: string;
query?: string;
@@ -209,7 +200,6 @@ export class GoogleMailManager implements MailManager {
{ folder, q, maxResults, _labelIds, pageToken, email: this.config.auth?.email },
);
}
public get(id: string) {
return this.withErrorHandler(
'get',
@@ -353,7 +343,6 @@ export class GoogleMailManager implements MailManager {
{ id, email: this.config.auth?.email },
);
}
public create(data: IOutgoingMessage) {
return this.withErrorHandler(
'create',
@@ -371,7 +360,6 @@ export class GoogleMailManager implements MailManager {
{ data, email: this.config.auth?.email },
);
}
public delete(id: string) {
return this.withErrorHandler(
'delete',
@@ -382,7 +370,6 @@ export class GoogleMailManager implements MailManager {
{ id },
);
}
public normalizeIds(ids: string[]) {
return this.withSyncErrorHandler(
'normalizeIds',
@@ -395,7 +382,6 @@ export class GoogleMailManager implements MailManager {
{ ids },
);
}
public modifyLabels(
threadIds: string[],
options: { addLabels: string[]; removeLabels: string[] },
@@ -411,7 +397,6 @@ export class GoogleMailManager implements MailManager {
{ threadIds, options },
);
}
public sendDraft(draftId: string, data: IOutgoingMessage) {
return this.withErrorHandler(
'sendDraft',
@@ -663,9 +648,6 @@ export class GoogleMailManager implements MailManager {
return false;
}
}
// ===============================================
private async modifyThreadLabels(
threadIds: string[],
requestBody: gmail_v1.Schema$ModifyThreadRequest,
@@ -990,7 +972,6 @@ export class GoogleMailManager implements MailManager {
rawMessage: draft.message,
};
}
private async withErrorHandler<T>(
operation: string,
fn: () => Promise<T> | T,
@@ -1037,16 +1018,15 @@ export class GoogleMailManager implements MailManager {
}
}
private findAttachments(parts: any[]): any[] {
let results: any[] = [];
private findAttachments(parts: gmail_v1.Schema$MessagePart[]): gmail_v1.Schema$MessagePart[] {
let results: gmail_v1.Schema$MessagePart[] = [];
for (const part of parts) {
if (part.filename && part.filename.length > 0) {
const contentDisposition =
part.headers?.find((h: any) => h.name?.toLowerCase() === 'content-disposition')?.value ||
'';
part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')?.value || '';
const isInline = contentDisposition.toLowerCase().includes('inline');
const hasContentId = part.headers?.some((h: any) => h.name?.toLowerCase() === 'content-id');
const hasContentId = part.headers?.some((h) => h.name?.toLowerCase() === 'content-id');
if (!isInline || (isInline && !hasContentId)) {
results.push(part);

View File

@@ -1,9 +1,10 @@
import type { MailManager, ManagerConfig } from './types';
import { OutlookMailManager } from './microsoft';
import { GoogleMailManager } from './google';
const supportedProviders = {
google: GoogleMailManager,
// microsoft: microsoftDriver,
microsoft: OutlookMailManager,
};
export const createDriver = (

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,8 @@ export interface IConfig {
export type ManagerConfig = {
auth: {
userId: string;
// accountId: string;
accessToken: string;
refreshToken: string;
email: string;

View File

@@ -38,6 +38,7 @@ export const getActiveDriver = async () => {
auth: {
accessToken: activeConnection.accessToken,
refreshToken: activeConnection.refreshToken,
userId: activeConnection.userId,
email: activeConnection.email,
},
});

439
bun.lock

File diff suppressed because it is too large Load Diff