mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 00:06:17 +00:00
feat: offer the code of sign-in and verification mails for copying in the list and the reader
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
import { formatSize } from '../../lib/attachment-display';
|
||||
import { isSmimeEmail } from '../../lib/smime';
|
||||
import { resolveEmailTags } from '../../lib/thread-utils';
|
||||
import { findVerificationCode, verificationCodeBodyText } from '../../lib/verification-code';
|
||||
import { VerificationCodeChip } from './VerificationCodeChip';
|
||||
|
||||
interface Props {
|
||||
email: Email;
|
||||
@@ -99,6 +101,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||
const keywordDefs = useKeywordsStore((s) => s.keywords);
|
||||
const showVerificationCodes = useSettingsStore((s) => s.showVerificationCodes);
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
React.useEffect(() => { setShowDetails(false); }, [email.id]);
|
||||
|
||||
@@ -114,6 +117,15 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
||||
[info.auth, from?.email],
|
||||
);
|
||||
const verificationText = verification ? describeSenderVerification(verification, t) : null;
|
||||
// The sign-in code of the message, offered for copying. Looked up again
|
||||
// only when the message or its loaded body changes, not on a keyword change.
|
||||
const verificationCode = React.useMemo(
|
||||
() => (showVerificationCodes && !compact
|
||||
? findVerificationCode(email.subject, verificationCodeBodyText(email))
|
||||
: null),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[showVerificationCodes, compact, email.id, email.subject, email.bodyValues],
|
||||
);
|
||||
const verificationColor = verification?.status === 'failed' ? c.error : c.warning;
|
||||
const verificationBg = verification?.status === 'failed' ? c.errorBg : c.warningBg;
|
||||
|
||||
@@ -253,6 +265,8 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
||||
</View>
|
||||
)}
|
||||
|
||||
{verificationCode && <VerificationCodeChip code={verificationCode} />}
|
||||
|
||||
{!compact && verificationText && (
|
||||
<View style={[styles.verificationNotice, { borderColor: verificationColor }]} accessibilityRole="alert">
|
||||
<ShieldAlert size={18} color={verificationColor} />
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { Check, Key } from '../icons';
|
||||
import type { Email } from '../../api/types';
|
||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { useSettingsStore } from '../../stores/settings-store';
|
||||
import { toast } from '../../stores/toast-store';
|
||||
import { listVerificationCode } from '../../lib/verification-code';
|
||||
|
||||
/** The code a list row offers: found in the subject and preview of a fresh mail. */
|
||||
export function useListVerificationCode(email: Pick<Email, 'subject' | 'preview' | 'receivedAt'>): string | null {
|
||||
const enabled = useSettingsStore((s) => s.showVerificationCodes);
|
||||
const { subject, preview, receivedAt } = email;
|
||||
return React.useMemo(
|
||||
() => (enabled ? listVerificationCode({ subject, preview, receivedAt }) : null),
|
||||
[enabled, subject, preview, receivedAt],
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
/** A list row in selection mode: the tap selects the row instead. */
|
||||
disabled?: boolean;
|
||||
/** Spacing for a list row (below the preview). */
|
||||
inList?: boolean;
|
||||
}
|
||||
|
||||
/** The one-time code of a sign-in mail (lib/verification-code.ts); a tap copies it. */
|
||||
export function VerificationCodeChip({ code, disabled, inList }: Props) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copied) return undefined;
|
||||
const timer = setTimeout(() => setCopied(false), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
const copy = () => {
|
||||
Clipboard.setStringAsync(code).then(
|
||||
() => {
|
||||
setCopied(true);
|
||||
toast.success(t('email_viewer.verification_code.copied', 'Code copied'));
|
||||
},
|
||||
() => toast.error(t('email_viewer.verification_code.copy_failed', 'Could not copy the code')),
|
||||
);
|
||||
};
|
||||
|
||||
const Icon = copied ? Check : Key;
|
||||
const label = t('email_viewer.verification_code.copy', 'Copy code {code}', { code });
|
||||
return (
|
||||
<View style={[styles.row, inList && styles.rowInList]}>
|
||||
<Pressable
|
||||
onPress={copy}
|
||||
disabled={disabled}
|
||||
hitSlop={6}
|
||||
style={({ pressed }) => [styles.chip, pressed && styles.chipPressed]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={label}
|
||||
testID="verification-code-chip"
|
||||
>
|
||||
<Icon size={12} color={copied ? c.success : c.warning} />
|
||||
<Text style={styles.code} numberOfLines={1}>{code}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
row: { flexDirection: 'row' },
|
||||
rowInList: { marginTop: spacing.xs },
|
||||
chip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: c.surface,
|
||||
},
|
||||
chipPressed: { backgroundColor: c.surfaceHover },
|
||||
code: { ...typography.caption, color: c.text, fontWeight: '600', fontFamily: 'monospace', letterSpacing: 0.5 },
|
||||
});
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export function ReadingSettings() {
|
||||
const deleteAction = useSettingsStore((s) => s.deleteAction);
|
||||
const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk);
|
||||
const showPreview = useSettingsStore((s) => s.showPreview);
|
||||
const showVerificationCodes = useSettingsStore((s) => s.showVerificationCodes);
|
||||
const clearSearchOnFolderChange = useSettingsStore((s) => s.clearSearchOnFolderChange);
|
||||
const disableThreading = useSettingsStore((s) => s.disableThreading);
|
||||
const includeGroupInUnified = useSettingsStore((s) => s.includeGroupInUnified);
|
||||
@@ -193,6 +194,10 @@ export function ReadingSettings() {
|
||||
<ToggleSwitch checked={showPreview} onChange={(v) => update('showPreview', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('settings.email_behavior.show_verification_codes.label', "Show Verification Codes")} description={t('settings.email_behavior.show_verification_codes.description', "Offer the code from sign-in and confirmation emails for one-click copying, in the list for a day and in the message")}>
|
||||
<ToggleSwitch checked={showVerificationCodes} onChange={(v) => update('showVerificationCodes', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('settings.email_behavior.disable_threading.label', "Disable Conversation Grouping")} description={t('settings.email_behavior.disable_threading.description', "Show emails as individual messages instead of grouped by conversation")}>
|
||||
<ToggleSwitch checked={disableThreading} onChange={(v) => update('disableThreading', v)} />
|
||||
</SettingItem>
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
[
|
||||
{
|
||||
"sender": "AliExpress",
|
||||
"subject": "Ihr AliExpress-Verifizierungscode",
|
||||
"preview": "\nHallo Bitte den 6-stelligen Code auf der nachfolgenden E-Mail-Verifizierungsseite eingeben:\n 206757\nDenken Sie daran: Schützen Sie sich vor Missbrauch und bewahren Sie diesen einmaligen Verifizierungscode vertraulich auf.\nVielen Dank,\nDas AliExpress-T...",
|
||||
"body": "Hallo Bitte den 6-stelligen Code auf der nachfolgenden E-Mail-Verifizierungsseite eingeben:\n206757\n Denken Sie daran: Schützen Sie sich vor Missbrauch und bewahren Sie diesen einmaligen Verifizierungscode vertraulich auf.\nVielen Dank,\n Das AliExpress-Team",
|
||||
"code": "206757"
|
||||
},
|
||||
{
|
||||
"sender": "Anthropic",
|
||||
"subject": "Your verification code is 177945",
|
||||
"preview": "Sign in to Claude.ai177945Copy and paste the temporary verification code to sign in. If you didn't try to sign in, you can safely ignore this email.Claude.ai • Research • Product • Company",
|
||||
"body": "Sign in to Claude.ai\n177945\nCopy and paste the temporary verification code to sign in. If you didn't try to sign in, you can safely ignore this email.\nClaude.ai <https://url8792.mail.anthropic.com/link?t=0f3a9c21> • Research <https://url8792.mail.anthropic.com/link?t=0f3a9c21> • Product <https://url8792.mail.anthropic.com/link?t=0f3a9c21> • Company <https://url8792.mail.anthropic.com/link?t=0f3a9c21>",
|
||||
"code": "177945"
|
||||
},
|
||||
{
|
||||
"sender": "Volkswagen Group",
|
||||
"subject": "Einmalkennwort für die Verifizierung des Kandidatenkontos",
|
||||
"preview": " Hallo,\n\nIhr Passcode für die Kontoverifizierung lautet 854857\n\nGeben Sie diesen Passcode auf der Seite „Karrierechancen – Konto erstellen“ ein, um Ihr Konto zu verifizieren.\n\nDieser Passcode ist 10 Minuten lang gültig.",
|
||||
"body": "Hallo,\n\nIhr Passcode für die Kontoverifizierung lautet 854857\n\nGeben Sie diesen Passcode auf der Seite „Karrierechancen – Konto erstellen“ ein, um Ihr Konto zu verifizieren.\n\nDieser Passcode ist 10 Minuten lang gültig.",
|
||||
"code": "854857"
|
||||
},
|
||||
{
|
||||
"sender": "Epic Games",
|
||||
"subject": "Dein Zwei-Faktor-Anmeldecode",
|
||||
"preview": "\u00a0 \u00a0 \u00a0 Dein Zwei-Faktor-Anmeldecode: 533964 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 Hallo alexm,\n\n\u00a0 \u00a0 Du hast kürzlich versucht, dich von einem neuen Gerät, Browser oder Standort anzumelden. Verwende bitte zur Vervollständigung deiner Anmeldung den obigen Code. Erfahre hi...",
|
||||
"body": "Dein Zwei-Faktor-Anmeldecode:\n 533964\n Hallo alexm,\n\n Du hast kürzlich versucht, dich von einem neuen Gerät, Browser oder Standort anzumelden. Verwende bitte zur Vervollständigung deiner Anmeldung den obigen Code. Erfahre hier mehr <https://accts.epicgames.com/link?t=0f3a9c21> .\n\n Wenn die Anfrage nicht von dir stammt, bitte ändere dein Passwort. <https://accts.epicgames.com/link?t=0f3a9c21> .\n\n Vielen Dank!\nDas Epic Games-Team\n Brauchst du Hilfe? help.epicgames.com <https://accts.epicgames.com/link?t=0f3a9c21>\n © 2024 Epic Games, Inc. Alle Rechte vorbehalten. Epic, Epic Games, das Logo von Epic Games, Unreal, Unreal Engine, das Logo von Unreal Engine, Epic Games Store und das Logo von Epic ",
|
||||
"code": "533964"
|
||||
},
|
||||
{
|
||||
"sender": "Bolt",
|
||||
"subject": "0371 ist dein Bestätigungscode",
|
||||
"preview": "Lass uns deinen Account reaktivieren., Alex! Verwende den unten stehenden Code in der App. 0371\nDer Code läuft ab in 15 Min..\nDu hast diesen Code nicht angefordert? Hier klicken Service provider Bolt Operations OÜ Vana-Lõuna 15, 10135 Tallinn, Eston...",
|
||||
"body": "Lass uns deinen Account reaktivieren., Alex! Verwende den unten stehenden Code in der App.\n 0371\n Der Code läuft ab in 15 Min..\nDu hast diesen Code nicht angefordert? Hier klicken <https://3zf1wp45.r.eu-central-1.awstrack.me/link?t=0f3a9c21>\nService provider Bolt Operations OÜ\nVana-Lõuna 15, 10135 Tallinn, Estonia\nGemacht mit\nDu bist dir nicht sicher, warum du diese E-Mail erhalten hast? <https://3zf1wp45.r.eu-central-1.awstrack.me/link?t=0f3a9c21>",
|
||||
"code": "0371"
|
||||
},
|
||||
{
|
||||
"sender": "Sendcloud",
|
||||
"subject": "[Sendcloud] E-Mail-Adresse bestätigen",
|
||||
"preview": "Diese E-Mail enthält den E-Mail-Bestätigungscode, den Sie verwenden können, um Ihre E-Mail-Adresse bei Sendcloud zu verifizieren. Der Bestätigungscode ist unten aufgeführt. Bitte geben Sie ihn in Ihr geöffnetes Browserfenster ein.\n\n37CCHC",
|
||||
"body": "E-Mail-Adresse bestätigen\n Welcome to Sendcloud and thank you for your sign-up! In order to complete your sign-up, we are asking you to verify your email address. The email verification code is listed below. Please enter it in your open browser window.\n37CCHC\n Beste Grüße,\n Sendcloud\n Sendcloud\ncontact@sendcloud.com",
|
||||
"code": "37CCHC"
|
||||
},
|
||||
{
|
||||
"sender": "TP-Link",
|
||||
"subject": "Tapo-Konto-Verifizierungscode",
|
||||
"preview": " Hallo,Ihr Tapo-Konto-Verifizierungscode lautet: 244027 Dieser Code läuft in 10 Minuten ab. Geben Sie den Code nicht an Dritte weiter.\n\nWenn Sie den Code nicht angefordert haben, versucht möglicherweise jemand, auf Ihr Konto zuzugreifen. Sie können I...",
|
||||
"body": "Hallo,\nIhr Tapo-Konto-Verifizierungscode lautet:\n244027\n Dieser Code läuft in 10 Minuten ab. Geben Sie den Code nicht an Dritte weiter.\n\n Wenn Sie den Code nicht angefordert haben, versucht möglicherweise jemand, auf Ihr Konto zuzugreifen. Sie können Ihr Passwort ändern, um Ihr Konto zu sichern.",
|
||||
"code": "244027"
|
||||
},
|
||||
{
|
||||
"sender": "BundID",
|
||||
"subject": "BundID: Ihr angeforderter Sicherheitscode",
|
||||
"preview": "\n\nHallo, \nIhr persönlicher Code für Ihre Aktion bei der BundID lautet:\n\n\n749054\n\n\nGeben Sie diesen Code ein, um die Aktion sicher abzuschließen. Bitte teilen \nSie den Code nicht mit Dritten.\n\n\nMit freundlichen Grüßen\n\nBundID \n\n\n\nBitte antworten Sie...",
|
||||
"body": "Hallo,\nIhr persönlicher Code für Ihre Aktion bei der BundID lautet:\n\n749054\n\nGeben Sie diesen Code ein, um die Aktion sicher abzuschließen. Bitte teilen Sie den Code nicht mit Dritten.\n\nMit freundlichen Grüßen\nBundID\n\nBitte antworten Sie nicht auf diese E-Mail. Das Postfach wird nicht überwacht und Sie werden keine Antwort erhalten. Wenn Sie Fragen oder Anmerkungen haben, kontaktieren Sie uns bitte über dieses Formular: Kontaktformular <https://id.bund.de/link?t=0f3a9c21>",
|
||||
"code": "749054"
|
||||
},
|
||||
{
|
||||
"sender": "Lieferando",
|
||||
"subject": "Dein Bestätigungscode",
|
||||
"preview": "Gib diesen Code ein, um dich bei Lieferando anzumelden. \u00a0\u200c\u00a0\u200c\u00a0\u200c\u00a0\u200c\u00a0\u200c\u00a0\u200c...",
|
||||
"body": "Gib diesen Code ein, um dich bei Lieferando anzumelden.\n \u200c \u200c \u200c \u200c \u200c \u200c \n Dein Bestätigungscode\n Gib diesen Code ein, um dich anzumelden:\n C5EX8R\n Dieser Code ist für 10 Minuten gültig.\n\n Gib diesen Code nicht an Dritte weiter.\n Lieferando\n Beachte bitte, dass Lieferando dich niemals nach deinen Kontodaten fragen wird.\nDerartige Anfragen solltest du einfach löschen.\nWir empfehlen außerdem, dein Passwort regelmäßig zu ändern.\n Lieferando | Lieferando ist ein Angebot von Takeaway.com\nJust Eat Take\u200baway\u200b.com N.V. | Piet Heinkade 61 | 1019 GM Ams\u00adter\u00addam",
|
||||
"code": "C5EX8R"
|
||||
},
|
||||
{
|
||||
"sender": "PPSPY",
|
||||
"subject": "Complete your registration",
|
||||
"preview": "Email Verification\nYour email verification code is :\n5370\nPlease copy this activation code and enter this code in the page to activate your account.\n",
|
||||
"body": "Email Verification\nYour email verification code is :\n 5370\nPlease copy this activation code and enter this code in the page to activate your account.",
|
||||
"code": "5370"
|
||||
},
|
||||
{
|
||||
"sender": "Messe Frankfurt",
|
||||
"subject": "Messe-Login: Bestätigen Sie Ihre Aktion",
|
||||
"preview": "Guten Tag,\n\nzum Abschluss des Vorgangs ist die Eingabe eines Bestätigungscodes notwendig. Ihr Code lautet:\n\n937480\n\nViele Grüße\nIhr Messe-Login-Team\n--\nMesse Frankfurt GmbH\nLudwig-Erhard-Anlage 1\n60327 Frankfurt am Main\nwww.messefrankfurt.com\nmesse-l...",
|
||||
"body": "Guten Tag,\n\n zum Abschluss des Vorgangs ist die Eingabe eines Bestätigungscodes notwendig. Ihr Code lautet:\n937480\n\n Viele Grüße\n Ihr Messe-Login-Team\n --\n Messe Frankfurt GmbH\n Ludwig-Erhard-Anlage 1\n 60327 Frankfurt am Main\nwww.messefrankfurt.com <https://www.messefrankfurt.com/link?t=0f3a9c21>\nmesse-login@messefrankfurt.com\n\n Geschäftsführung: Wolfgang Marzin (Vorsitzender), Detlef Braun\n Amtsgericht Frankfurt am Main HRB 58331\n\n Messe-Login ist ein Service der Messe Frankfurt",
|
||||
"code": "937480"
|
||||
},
|
||||
{
|
||||
"sender": "Planauskunft",
|
||||
"subject": "Registrierung erfolgreich",
|
||||
"preview": "Registrierung erfolgreich Lovion Registrierung erfolgreich Bitte klicken Sie auf den nachfolgenden Link, um Ihre Registrierung abzuschließen:\nhttps://planauskunft.lsw.de/link?t=0f3a9c21\nAlte...",
|
||||
"body": "Registrierung erfolgreich\n Lovion\n Registrierung erfolgreich\n Bitte klicken Sie auf den nachfolgenden Link, um Ihre Registrierung abzuschließen:\nhttps://planauskunft.lsw.de/link?t=0f3a9c21\n Alternativ können Sie den Code b41c9d07-2385-46a1 auf dieser Seite verwenden:\nhttps://planauskunft.lsw.de/link?t=0f3a9c21\nCopyright © Lovion GmbH <https://lovion.de/link?t=0f3a9c21>\nPhoenixseestraße 6, 44263 Dortmund",
|
||||
"code": "b41c9d07-2385-46a1"
|
||||
},
|
||||
{
|
||||
"sender": "Bitwarden",
|
||||
"subject": "Your Bitwarden Verification Code",
|
||||
"preview": "To finish logging in, enter this verification code: 083447\n\nIf this was not you, take these immediate steps to secure your account in the web app:\n\nDeauthorize unrecognized devices\n\nChange your master password\n\nTurn on two-step login\n\nAccount : info@lin...",
|
||||
"body": "To finish logging in, enter this verification code: 083447\n\n If this was not you, take these immediate steps to secure your account in the web app <https://vault.bitwarden.com/link?t=0f3a9c21> :\nDeauthorize unrecognized devices\nChange your master password\nTurn on two-step login\n\nAccount: alex@example.org\nDate: Monday, December 8, 2025 at 11:01 PM UTC\nIP: 93.231.39.68\nDeviceType: Firefox\n © 2025 Bitwarden Inc.",
|
||||
"code": "083447"
|
||||
},
|
||||
{
|
||||
"sender": "Mozilla",
|
||||
"subject": "Verwenden Sie 347147 zur Anmeldung",
|
||||
"preview": "\n\nHaben Sie sich bei Firefox for Android angemeldet?\n\nHelfen Sie uns, Ihr Konto zu schützen, indem Sie Ihre Anmeldung genehmigen:\n\nFirefox Mobile auf Android 16\n\nFreitag, 12. Dez. 2025\n10:49:27 (CET)\n\nWenn ja, ist hier Ihr Autorisierungscode:\n\n347147\n\n...",
|
||||
"body": "Dieser Code läuft in 5 Minuten ab.\nHaben Sie sich bei Firefox for Android angemeldet?\nHelfen Sie uns, Ihr Konto zu schützen, indem Sie Ihre Anmeldung genehmigen:\nFirefox Mobile auf Android 16\n Freitag, 12. Dez. 2025\n 10:49:27 (CET)\nWenn ja, ist hier Ihr Autorisierungscode:\n347147\nEr läuft in 5 Minuten ab.\nWenn Sie diese Maßnahme nicht ausgelöst haben, ändern Sie sofort Ihr Passwort <https://accounts.firefox.com/link?t=0f3a9c21> . Weitere Informationen erhalten Sie bei der Mozilla-Hilfe <https://support.mozilla.org/link?t=0f3a9c21> .\nMozilla. 149 New Montgomery St, 4th Floor, San Francisco, CA 94105\nDatenschutzhinweis von Mozilla-Konten <https://www.mozilla.org/link?t=0f3a9c21>\nNutzungsbedingungen von Mozilla-Konten <https://www.mozilla.org/link?t=0f3a9c21>",
|
||||
"code": "347147"
|
||||
},
|
||||
{
|
||||
"sender": "Bluesky",
|
||||
"subject": "Password Reset Requested",
|
||||
"preview": "Bluesky [https://bsky.social/link?t=0f3a9c21]\n\n\nRESET PASSWORD\n\nWe received a request to reset the password for the account\n@alex.example.org.\n\nNJF63-SW7S7\n\nTo choose a new password, please enter the code above in the app a...",
|
||||
"body": "We received a request to reset the password for the account @alex.example.org.\n \u200c\u200b\u200d\u200e\u200f \u200c\u200b\u200d\u200e\u200f \nReset password\nWe received a request to reset the password for the account @ testacc.example.org .\nNJF63-SW7S7\nTo choose a new password, please enter the code above in the app along with your new password.\nBluesky <https://bsky.app/link?t=0f3a9c21> , the social internet",
|
||||
"code": "NJF63-SW7S7"
|
||||
},
|
||||
{
|
||||
"sender": "Alibaba",
|
||||
"subject": "548980 ist Ihr Bestätigungscode auf Alibaba.com",
|
||||
"preview": "\u00a0\n\n\u200c \u200c \u200c \u200c \u200c \u200c ...",
|
||||
"body": "\u200c \u200c \u200c \u200c \u200c \u200c \n Hi, Alex Muster,\n Um den Überprüfungsprozess abzuschließen, geben Sie den folgenden Code in unsere App oder Website ein:\n 548980\n Bitte bewahren Sie Ihre persönlichen Daten sicher auf, um einen unbefugten Zugriff auf Ihr Konto zu verhindern.\n Wenn Sie keinen Bestätigungscode von uns angefordert haben, können Sie diese Nachricht ignorieren.\n Antworten Sie nicht auf diese automatisch generierte E-Mail-Nachricht.\n Alle Rechte vorbehalten. © 1999-2026 Alibaba.com",
|
||||
"code": "548980"
|
||||
},
|
||||
{
|
||||
"sender": "MediaMarkt",
|
||||
"subject": "Dein Verifizierungscode lautet: 304627",
|
||||
"preview": "Bestätigen dein neues MediaMarkt-Saturn Onlinekonto\nBitte gebe folgenden Bestätigungs-Code ein: 3 0 4 6 2 7 Gebe diesen Code aus Sicherheitsgründen nicht an Dritte weiter, um dein Kundenkonto vor einem unbefugten Zugriff zu schützen. Nach Eingabe de...",
|
||||
"body": "Bestätigen dein neues MediaMarkt-Saturn Onlinekonto\n Bitte gebe folgenden Bestätigungs-Code ein:\n 3 0 4 6 2 7\n Gebe diesen Code aus Sicherheitsgründen nicht an Dritte weiter, um dein Kundenkonto vor einem unbefugten Zugriff zu schützen.\n Nach Eingabe des Codes kannst du deine Registrierung abschließen.\n\n Falls du die Anfrage nicht gestellt hast, ignoriere diese E-Mail einfach.\n\n Dein MediaMarkt-Saturn Team\nwww.mediamarkt.de <https://www.mediamarkt.de/link?t=0f3a9c21>\n Media-Saturn Deutschland GmbH\n Media-Saturn-Straße 1\n D-85053 Ingolstadt\n E-Mail: datenschutz@mediamarkt.de\n Tel: 0221/22243-333\n Geschäftsführer: Bas Eijssink, Mario Pieper, Thomas Fernkorn, Tom Alexander Rauchut, Carsten Geilert\n Eingetragen beim Amtsgericht Ingolstadt unter HRB3429\n\nDatenschutzhinweise <https://www.mediamarkt.de/link?t=0f3a9c21>",
|
||||
"code": "304627"
|
||||
},
|
||||
{
|
||||
"sender": "No-IP",
|
||||
"subject": "No-IP Verification Code: 325682",
|
||||
"preview": "\nAt No-IP we take security seriously. A one time account verification PIN has been issued for extra security confirmation. This PIN is only valid for 10 minutes.\n\n\n325682\n\nIf this wasn't you, please change your password immediately to secure your a...",
|
||||
"body": "No-IP Verification Code\nFor account security purposes, please enter the following verification on our website:\n325682\nVerification code will expire in 10 minutes.\nIf this wasn't you, please change your password immediately to secure your account. We recommending enabling Two-Factor Authentication. If you have any questions or need assistance please open a Support-Ticket öffnen. or give us a call at <https://url9788.noip.com/link?t=0f3a9c21> 775.853.1883 <tel:7758531883> .\n No-IP\n 425 Maestro Dr. Suite 200\n Reno, Nevada 89511 USA\n +1 775-853-1883\n ©1999-2026 Vitalwerks Internet Solutions, LLC dba No-IP\n ©2026 No-IP Technologies, LLC All Rights Reserved.",
|
||||
"code": "325682"
|
||||
},
|
||||
{
|
||||
"sender": "Chefplatz",
|
||||
"subject": "Anmelden auf Chefplatz.de",
|
||||
"preview": "Bestätige deine Registrierung Hallo,\n\nVielen Dank für deine Registrierung. Hier ist dein 6-stelliger Bestätigungscode:\n 732888Gib diesen Code in der App ein, um deine Registrierung abzuschließen.\nWenn du diese Registrierung nicht angefordert hast, k...",
|
||||
"body": "Bestätige deine Registrierung\n Hallo,\n\n Vielen Dank für deine Registrierung. Hier ist dein 6-stelliger Bestätigungscode:\n732888\n Gib diesen Code in der App ein, um deine Registrierung abzuschließen.\n Wenn du diese Registrierung nicht angefordert hast, kannst du diese Nachricht ignorieren.",
|
||||
"code": "732888"
|
||||
},
|
||||
{
|
||||
"sender": "Wikipedia",
|
||||
"subject": "Your Wikipedia login verification code",
|
||||
"preview": "AlexMuster, it looks like you are trying to log in from a new device on Wikipedia.\n\nYour verification code is:\n\n253367\n\nThis code is required to access your account. It will expire in 1 day. For your security, do not share it with anyone.\n\nIf this wasn...",
|
||||
"body": "AlexMuster, it looks like you are trying to log in from a new device on Wikipedia.\nYour verification code is:\n 253367\nThis code is required to access your account. It will expire in 1 day. For your security, do not share it with anyone.\nIf this wasn’t you:\nThis email was sent because someone attempted to log in to your account. The login attempt included your correct account name and password.\nIf this was not you, you should change your password now.\nFor more information, see https://www.mediawiki.org/link?t=0f3a9c21",
|
||||
"code": "253367"
|
||||
},
|
||||
{
|
||||
"sender": "Microsoft",
|
||||
"subject": "Ihr Einmalcode",
|
||||
"preview": "Hallo alex@example.org,\n\nwir haben Ihre Anforderung für einen Einmalcode für Ihr Microsoft-Konto erhalten.\n\nIhr Einmalcode lautet: 992771\n\nGeben Sie diesen Code nur auf einer offiziellen Website oder App ein. Teilen Sie ihn mit niemandem. Außerhalb...",
|
||||
"body": "Hallo alex@example.org,\n\nwir haben Ihre Anforderung für einen Einmalcode für Ihr Microsoft-Konto erhalten.\n\nIhr Einmalcode lautet: 992771\n\nGeben Sie diesen Code nur auf einer offiziellen Website oder App ein. Teilen Sie ihn mit niemandem. Außerhalb einer offiziellen Plattform werden wir nie danach fragen.\n\nMit freundlichen Grüßen\nIhr Microsoft-Konto-Team\nDatenschutzerklärung\u200b: https://go.microsoft.com/link?t=0f3a9c21 | Impressum: https://aka.ms/link?t=0f3a9c21\nMicrosoft Corporation, One Microsoft Way, Redmond, WA 98052\n",
|
||||
"code": "992771"
|
||||
},
|
||||
{
|
||||
"sender": "Tuya",
|
||||
"subject": "【Tuya】Verification Code",
|
||||
"preview": "Dear user\nWe appreciate that you have applied for an account on Tuya IoT Platform,and we look forward to cooperating with you!To research and develop smart products on the platform,please copy the verification code and fill in the registration page.\nVer...",
|
||||
"body": "Dear user\nWe appreciate that you have applied for an account on Tuya IoT Platform,and we look forward to cooperating with you!To research and develop smart products on the platform,please copy the verification code and fill in the registration page.\nVerification code:\n261765\nIf the failure persists, please send this email to service@tuya.com ,we will take care of it ASAP.\n If you do not wish to receive such emails, please click here to unsubscribe. <https://mail-unsubscribe-cn.ismartlife.me/link?t=0f3a9c21>",
|
||||
"code": "261765"
|
||||
},
|
||||
{
|
||||
"sender": "otelo",
|
||||
"subject": "IHR OTELO PASSWORT",
|
||||
"preview": "IHR OTELO PASSWORT\n\nHallo Alex\u00a0Muster,\n\nSie haben einen Code zum Zurücksetzen Ihres Passworts angefordert.\n\nMit dem folgenden Code können Sie Ihr Passwort zurücksetzen und ein neues festlegen:\n\n026751\n\nSicherheitshinweis: Ihre Zugangsdaten und Passw...",
|
||||
"body": "IHR OTELO PASSWORT\n\nHallo Alex Muster,\nSie haben einen Code zum Zurücksetzen Ihres Passworts angefordert.\n\nMit dem folgenden Code können Sie Ihr Passwort zurücksetzen und ein neues festlegen:\n\n026751\n\nSicherheitshinweis : Ihre Zugangsdaten und Passwörter sind vertraulich. Wir werden Sie niemals per E-Mail, Telefon oder auf anderen Wegen nach diesen Informationen fragen. Geben Sie diesen Code und Ihre Zugangsdaten nur auf der offiziellen otelo-Website oder in der otelo-App ein. Bei verdächtigen Anfragen kontaktieren Sie uns bitte direkt.\n\nFalls Sie die Zurücksetzung nicht angefordert haben, können Sie diese E-Mail ignorieren.\n\nFreundliche Grüße\nIhr otelo-Team\nEINFACH MEHR. EINFACH FAIR. EINFA",
|
||||
"code": "026751"
|
||||
},
|
||||
{
|
||||
"sender": "IKEA",
|
||||
"subject": "Einmaliger Code für den Zugriff auf dein IKEA Konto.",
|
||||
"preview": "Hej,bitte gib die sechs Ziffern unten in die Ansicht ein, in der du den IKEA Einmalcode angefordert hast.724297Dieser Einmalcode ist nur 5 Minuten gültig.Vielen DankDein IKEA TeamWunderbarer Alltag ( https://www.instagram.com/link?t=0f3a9c21 ) ( https...",
|
||||
"body": "Hej,\nbitte gib die sechs Ziffern unten in die Ansicht ein, in der du den IKEA Einmalcode angefordert hast.\n724297\nDieser Einmalcode ist nur 5 Minuten gültig.\nVielen Dank\n\nDein IKEA Team\nWunderbarer Alltag\nEntdecke Neuigkeiten und Angebote in deinem IKEA Einrichtungshaus <https://www.ikea.com/link?t=0f3a9c21>\nKontaktiere uns <https://www.ikea.com/link?t=0f3a9c21> | Allgemeine Liefer- und Zahlungsbedingungen <https://www.ikea.com/link?t=0f3a9c21> | Datenschutzerklärung <https://www.ikea.com/link?t=0f3a9c21>\n© Inter IKEA Systems B.V. 1999-2025",
|
||||
"code": "724297"
|
||||
},
|
||||
{
|
||||
"sender": "HA Remote",
|
||||
"subject": "Verify your HA Remote email",
|
||||
"preview": "Your HA Remote verification code is 811083.\n\nThis code expires in 15 minutes.\nOpen https://home.ctech.media/link?t=0f3a9c21 to continue.",
|
||||
"body": "Your HA Remote verification code is:\n811083\nThis code expires in 15 minutes.\nOpen HA Remote <https://home.ctech.media/link?t=0f3a9c21> to continue.",
|
||||
"code": "811083"
|
||||
},
|
||||
{
|
||||
"sender": "Registercheck",
|
||||
"subject": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"preview": "E-Mail-Adresse bestätigen\n\u200a\nWir müssen Ihre E-Mail-Adresse alex@example.org verifizieren, bevor Sie auf\nIhr Konto zugreifen können. Geben Sie den unten stehenden Code in Ihrem\ngeöffneten Browserfenster ein.\n\u200a\n293089\n\nDieser Code läuft in 10 ...",
|
||||
"body": "Ihr Bestätigungscode lautet 293089. Dieser Code läuft in 10 Minuten ab. Falls Sie sich nicht bei Registercheck angemeldet haben, können Sie diese E-Mail ignorieren.\nE-Mail-Adresse bestätigen\nWir müssen Ihre E-Mail-Adresse alex@example.org verifizieren, bevor Sie auf Ihr Konto zugreifen können. Geben Sie den unten stehenden Code in Ihrem geöffneten Browserfenster ein.\n293089\nDieser Code läuft in 10 Minuten ab.\nFalls Sie sich nicht bei Registercheck angemeldet haben, können Sie diese E-Mail ignorieren. Möglicherweise hat jemand anderes versehentlich Ihre E-Mail-Adresse eingegeben.",
|
||||
"code": "293089"
|
||||
},
|
||||
{
|
||||
"sender": "REWE",
|
||||
"subject": "863733 ist dein Bestätigungscode",
|
||||
"preview": "Unsere aktuellen Aktionen und Neuigkeiten zur kommenden Woche!\n \nHallo,\ndiese E-Mailwird ausschließlich in einer HTML-Version versendet, die du über den folgenden Link erreichst:\n \nhttps://epost3.rewe.de/link?t=0f3a9c21",
|
||||
"body": "Damit kommst du in dein REWE Konto.\nSollte diese E-Mail nicht optimal dargestellt werden, klicke bitte hier <https://epost3.rewe.de/link?t=0f3a9c21>\nDein REWE Konto <https://epost3.rewe.de/link?t=0f3a9c21>\nHallo Alex,\ngib diesen Code ein, um dich in deinem REWE Konto anzumelden:\n\n863733\nDamit dein REWE Konto sicher bleibt, verliert der Code nach 10 Minuten seine Funktion. Aber keine Eile - wir schicken dir einen neuen, wann immer du möchtest.\n\nViele Grüße\ndein REWE Team\nDu hast noch Fragen? Wir sind für dich da.\nWir liefern dir Antworten und Lösungsvorschläge.\nHäufige Fragen und Antworten.\nZum Hilfecenter <https://epost3.rewe.de/link?t=0f3a9c21>\nMelde dich bei uns.\nZum Kontaktbereich <https://epost3.rewe.de/link?t=0f3a9c21>\nDatenschutz <https://epost3.rewe.de/link?t=0f3a",
|
||||
"code": "863733"
|
||||
},
|
||||
{
|
||||
"sender": "Telekom Geschäftskunden",
|
||||
"subject": "Bitte bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"preview": "<table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" role=\"presentation\" style=\"min-width: 100%; \" class=\"stylingblock-content-wrapper\"><tr><td class=\"stylingblock-content-wrapper camarker-inner\"><br>\nHallo,<br>\n<br>\ngeben Sie bitte den folgenden Best&au...",
|
||||
"body": "Bitte bestätigen Sie Ihre E-Mail-Adresse\n\nGuten Tag,\n\ngeben Sie bitte den folgenden Bestätigungscode in das Eingabefenster ein, das Ihnen angezeigt wird:\n\nF17DC\n\nZu Ihrer eigenen Sicherheit können wir so feststellen, dass die von Ihnen genannte E-Mail-Adresse korrekt ist und Sie Zugriff auf das Postfach haben.\nFalls Sie nicht fortfahren wollen, können Sie diese E-Mail einfach ignorieren.\n\nMit freundlichen Grüßen\nIhre Telekom\n\n© 2026 Telekom Deutschland GmbH\n\nDatenschutz <https://www.telekom.de/link?t=0f3a9c21> | gesetzliche Pflichtangaben <https://www.telekom.de/link?t=0f3a9c21>\nDies ist eine automatisch generierte Nachricht, bitte antworten Sie nicht auf diese E-Mail.",
|
||||
"code": "F17DC"
|
||||
},
|
||||
{
|
||||
"sender": "Slack",
|
||||
"subject": "Slack confirmation code: 6ZA-BRE",
|
||||
"preview": "Confirm your email address\n\nHere’s your confirmation code. You can copy it into the open browser window or click the link below to confirm this email address.\n\n6ZA-BRE\n\nConfirm and Sign In [https://go.slack.com/link?t=0f3a9c21",
|
||||
"body": "Confirm your email address. Here’s your confirmation code. You can copy it into the open browser window or click the link below to confirm this email address.\n \u200c \u200c \u200c \u200c \u200c \u200c \nConfirm your email address\n Here’s your confirmation code. You can copy it into the open browser window or click the link below to confirm this email address.\n6ZA-BRE\nConfirm and Sign In <https://go.slack.com/link?t=0f3a9c21>\nIf you didn’t request this email, there’s nothing to worry about — you can safely ignore it.\nOur Blog <https://slackhq.com/link?t=0f3a9c21> | Policies <https://slack.com/link?t=0f3a9c21> | Help Center <https://slack.com/link?t=0f3a9c21> | Slack Community <https://slack.com/link?t=0f3a9c21>\n\n©2026 Slack Technologies, LLC, a Salesforce company.\n415 Mission Street, 3rd Floor, San Francisco, CA 94105\n\nAll rights reserved.",
|
||||
"code": "6ZA-BRE"
|
||||
},
|
||||
{
|
||||
"sender": "LinkedIn",
|
||||
"subject": "Here's your verification code 001029",
|
||||
"preview": "Hi Alex,\n\nEnter the 6-digit code below to verify your identity and regain access to your LinkedIn account.\n\n001029\n\nThanks for helping us keep your account secure.\n\nThe LinkedIn Team\n\n----------------------------------------\n\nThis email was intended fo...",
|
||||
"body": "Please verify it's you.\n \u034f \u034f \u034f \u034f \u034f \u034f \nEnter the 6-digit code below to verify your identity and regain access to your LinkedIn account.\n001029\nThanks for helping us keep your account secure.\nThe LinkedIn Team\nWhen and where this happened:\nDate: August 28, 2026 at 7:54 PM GMT\nOperating System: Windows\nBrowser: Firefox\nApproximate Location: Wolfenbuttel, Niedersachsen, Germany\n Never miss an update with the LinkedIn app\n This email was intended for Alex Muster (“Jack of all trades, master of none, but oftentimes better than a master of one.”)\nLearn why we included this. <https://www.linkedin.com/link?t=0f3a9c21>\nYou are receiving LinkedIn notification emails.\nHelp <https://www.linkedin.com/lin",
|
||||
"code": "001029"
|
||||
},
|
||||
{
|
||||
"sender": "Amazon",
|
||||
"subject": "amazon.de: Sign-in attempt",
|
||||
"preview": "\n \n \nAlex, Someone is attempting to sign in to your account. When: Aug 30, 2026 12:37 PM Central European Summer Time Device: Mozilla Firefox Windows (Desktop) Near: Berlin, Germany\nIf this was you, your verification code is:\n331543\n \n\n\n\nIf you ...",
|
||||
"body": "Alex,\n\nSomeone is attempting to sign in to your account.\n\nWhen: Aug 30, 2026 12:37 PM Central European Summer Time\n Device: Mozilla Firefox Windows (Desktop)\n Near: Berlin, Germany\nIf this was you, your verification code is:\n331543\nIf you didn't request it: click here to deny. <https://www.amazon.de/link?t=0f3a9c21>\n Don’t share it with others.\nAmazon.de is a trading name for Amazon EU Sarl, for Amazon Europe Core Sarl, for Amazon Media EU Sarl and for Amazon EU Sarl, all of which have their registered office at 38 avenue John F. Kennedy, L-1855 Luxembourg and for the Amazon Digital Germany GmbH, with registered office at Domagkstr. 28, 80807 Munich, Germany. ©2026 Amazon.com, Inc. or its affiliates. Amazon and all ",
|
||||
"code": "331543"
|
||||
},
|
||||
{
|
||||
"sender": "Telekom Login",
|
||||
"subject": "Ihr Bestätigungscode",
|
||||
"preview": "img{display:block}#outlook a{padding:0}body{width:100%!important} body{-webkit-text-size-adjust:none}a a:link,a:visited{color:#369;font-weight:400;text-decoration:underline}.footerTable--hintText a,.footerTable--lastRowLinks a,.postContentDisclaimer a,i...",
|
||||
"body": "Guten Tag,\n geben Sie den folgenden Bestätigungscode in das Eingabefeld auf der aktuellen Seite Ihres Browsers ein:\n311213\n Geben Sie diesen Code nicht an andere weiter. Durch die Eingabe bestätigen Sie, dass diese E-Mail-Adresse korrekt ist und Sie Zugriff auf das Postfach haben.\n Falls Sie diese Aktion nicht selbst veranlasst haben, können Sie diese E-Mail einfach ignorieren und müssen nichts weiter tun.\n Freundliche Grüße\n Ihre Telekom\n Die gesetzlichen Pflichtangaben finden Sie unter:\nwww.telekom.de/pflichtangaben <https://www.telekom.de/link?t=0f3a9c21>\n 2-KO-IAM-AM2-1\nTelekom Deutschland GmbH\nHilfe & Service <https://www.telekom.de/link?t=0f3a9c21> Datenschutz <https://www.telekom.de/l",
|
||||
"code": "311213"
|
||||
},
|
||||
{
|
||||
"sender": "Kleinanzeigen",
|
||||
"subject": "Passwort zurücksetzen",
|
||||
"preview": "\n\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tPasswort zurücksetzen | Dein Code 704626\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tHallo,\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tdu möchtest dein ...",
|
||||
"body": "Passwort zurücksetzen | Dein Code 704626\n Hallo,\n\n du möchtest dein Passwort für dein Kleinanzeigen-Konto zurücksetzen.\n\n Gib diesen Code auf Kleinanzeigen ein:\n\n 704626\n\n Der Code ist 5 Minuten gültig.\n\n Du hast keine Zurücksetzung angefordert? Dann kannst du diese E-Mail ignorieren.\n Bis bald auf Kleinanzeigen!\n Bitte antworte nicht auf diese E-Mail.\n Wenn du Fragen hast, hilft dir unser Hilfebereich <https://themen.kleinanzeigen.de/link?t=0f3a9c21> weiter.\nkleinanzeigen.de GmbH, Dernburgstrasse 50, 14057 Berlin, Deutschland <https://www.kleinanzeigen.de/link?t=0f3a9c21>\n Geschäftsführer: Paul Heimann, HRB-Nr.: 272623 B,\n Amtsgericht Charlottenburg, Sitz der Gesellschaft: Berlin\nAllgemein",
|
||||
"code": "704626"
|
||||
},
|
||||
{
|
||||
"sender": "TikTok Shop",
|
||||
"subject": "TikTok Shop Partner Center verification code",
|
||||
"preview": "Dear Partner,\nTo verify your account, enter this code in TikTok Shop :\n 24VURS\nVerification codes expire after 2 hours.\nDo not forward or give this code to anyone.\nThis is an automatically generated email. Replies to this email address aren't monitored....",
|
||||
"body": "Dear Partner,\nTo verify your account, enter this code in TikTok Shop :\n24VURS\nVerification codes expire after 2 hours.\nDo not forward or give this code to anyone.\nThis is an automatically generated email. Replies to this email address aren't monitored.\n\nBest regards,\nTikTok Shop Partner Center",
|
||||
"code": "24VURS"
|
||||
},
|
||||
{
|
||||
"sender": "Steam",
|
||||
"subject": "Ihr Steam-Account: Zugriff von einem neuen Web- oder Mobilgerät",
|
||||
"preview": "\nHallo alexm,\n\nEs scheint, als ob Sie versuchen, sich über ein neues Gerät anzumelden. Bitte\nverwenden Sie den folgenden Steam-Guard-Code, um auf Ihren Account zuzugreifen:\n\nAnfrage von\nCalifornia, Vereinigte Staaten von Amerika\n\nAnmeldecode\nY9DG...",
|
||||
"body": "alexm,\nEs scheint, als ob Sie versuchen, sich über ein neues Gerät anzumelden. Bitte verwenden Sie den folgenden Steam-Guard-Code, um auf Ihren Account zuzugreifen:\n Anfrage von\n California, Vereinigte Staaten von Amerika\n Y9DGP\n\nDas waren nicht Sie?\nDiese E-Mail wurde gesendet, weil jemand versucht hat, sich auf Ihrem Steam-Account anzumelden. Beim Anmeldeversuch wurde der korrekte Name und das korrekte Passwort Ihres Accounts verwendet.\n\n Falls dieser Anmeldeversuch nicht von Ihnen vorgenommen wurde, sollten Sie Ihr Steam-Passwort umgehend zurücksetzen <https://help.steampowered.com/link?t=0f3a9c21> .\n\n Der in dieser E-Mail enthaltene Anmeldecode wird benötigt, um auf Ihren Account zuzugreifen. Teilen Sie diese",
|
||||
"code": "Y9DGP"
|
||||
},
|
||||
{
|
||||
"sender": "Google",
|
||||
"subject": "Google-Bestätigungscode",
|
||||
"preview": "Google-Bestätigungscode\n\n\n\n\n\n\n\n\n\nLieber Google-Nutzer,\n\nwir haben eine Anfrage für den Zugriff auf Ihr Google-Konto \nalex@example.org über Ihre E-Mail-Adresse erhalten. Ihr \nGoogle-Bestätigungscode lautet:\n\n\n067647\n\n\nFalls Sie diesen Code ...",
|
||||
"body": "Google-Bestätigungscode\nLieber Google-Nutzer,\nwir haben eine Anfrage für den Zugriff auf Ihr Google-Konto alex@example.org über Ihre E-Mail-Adresse erhalten. Ihr Google-Bestätigungscode lautet:\n067647\nFalls Sie diesen Code nicht angefordert haben, versucht möglicherweise eine andere Person, auf das Google-Konto alex@example.org zuzugreifen. Geben Sie diesen Code nicht weiter.\nSie haben diese Nachricht erhalten, weil diese E-Mail-Adresse als Wiederherstellungs-E-Mail-Adresse für das Google-Konto alex@example.org angegeben wurde. Sollte dies nicht stimmen, klicken Sie bitte hier <https://accounts.google.com/link?t=0f3a9c21> , um Ihre E-Mail-Adresse aus diesem Google-Konto zu entfernen.\nMit ",
|
||||
"code": "067647"
|
||||
},
|
||||
{
|
||||
"sender": "ChatGPT",
|
||||
"subject": "Your temporary ChatGPT login code",
|
||||
"preview": "\nEnter this temporary verification code to continue:\n714810\nIf you were not trying to log in to ChatGPT, please reset your password.\n\nBest,\nThe ChatGPT team\n\n ChatGPT\nHelp center\n",
|
||||
"body": "Enter this temporary verification code to continue:\n 714810\n If you were not trying to log in to ChatGPT, please reset your password <https://url3243.email.openai.com/link?t=0f3a9c21> .\n\nBest,\nThe ChatGPT team\nChatGPT <https://url3243.email.openai.com/link?t=0f3a9c21>\nHelp center <https://url3243.email.openai.com/link?t=0f3a9c21>",
|
||||
"code": "714810"
|
||||
},
|
||||
{
|
||||
"sender": "Facebook",
|
||||
"subject": "86771674 ist dein Facebook-Code.",
|
||||
"preview": "Hallo Alex,\n\nNur noch ein Schritt, um dein Konto zu verifizieren\n\nHallo Alex,Wir haben von jemandem, der Facebook verwendet, eine Anfrage zur Verifizierung deines Kontos erhalten. Gib diesen Code ein:86771674Teile diesen Code mit niemandem.Wenn dich j...",
|
||||
"body": "Alex, nur noch ein Schritt, um dein Konto zu verifizieren.\nAlex Muster\nNur noch ein Schritt, um dein Konto zu verifizieren\nHallo Alex,\nWir haben von jemandem, der Facebook verwendet, eine Anfrage zur Verifizierung deines Kontos erhalten. Gib diesen Code ein:\n86771674\nTeile diesen Code mit niemandem.\n\nWenn dich jemand nach diesem Code fragt\nTeile diesen Code mit niemandem, insbesondere nicht, wenn dir die Person sagt, dass sie für Facebook oder Meta arbeitet. Möglicherweise handelt es sich um einen Versuch, dein Konto zu hacken.\n\nDie Anfrage kam nicht von dir?\nWenn du diese E-Mail erhalten hast, aber gar nicht versuchst, dein Konto zu verifizieren, teile uns das mit <https://www.facebook.com/link?t=0f3a9c21> . Solange du diesen Code mit niemandem tei",
|
||||
"code": "86771674"
|
||||
},
|
||||
{
|
||||
"sender": "Autodesk",
|
||||
"subject": "Einmaliger Passcode",
|
||||
"preview": "Autodesk ( https://u51843908.ct.sendgrid.net/link?t=0f3a9c21",
|
||||
"body": "Einmaliger Passcode\n Hi Alex Muster!\n\n Geben Sie den einmaligen Passcode ein, um die Aktion für Ihr Autodesk-Konto abzuschließen.\n\nCode:\n747189\n\nGeben Sie diesen Code nicht an Dritte weiter und leiten Sie diese E-Mail nicht weiter.\nAutodesk, Inc. • One Market, Ste. 400 San Francisco, CA 94105, USA\n© Autodesk, Inc. All rights reserved. Rechtliche Hinweise & Marken <https://u51843908.ct.sendgrid.net/link?t=0f3a9c21> . Autodesk-Datenschutzerklärung <https://u51843908.ct.sendgrid.net/link?t=0f3a9c21>\nDies ist eine vertragsrelevante E-Mail.\nAntworten Sie nicht auf diese E-Mail. Antworten auf diese E-Mail werden weder gelesen noch beantwortet.\nAutodesk und das Autodesk-Logo sind in den USA und/ode",
|
||||
"code": "747189"
|
||||
},
|
||||
{
|
||||
"sender": "Booking.com",
|
||||
"subject": "Booking.com\u00a0– HUK6QR ist Ihr Bestätigungscode",
|
||||
"preview": " Booking.com\n\nE-Mail-Adresse bestätigen\n\n Guten Tag alex@example.org,\n\n Dies ist Ihr individueller Code, mit dem Sie Ihre E-Mail-Adresse\n bestätigen können..\n HUK6QR\n\n Bitte beachten Sie, dass dieser Code nur einmal verwendet werden kann...",
|
||||
"body": "E-Mail-Adresse bestätigen\nGuten Tag alex@example.org,\nDies ist Ihr individueller Code, mit dem Sie Ihre E-Mail-Adresse bestätigen können..\nHUK6QR\n Bitte beachten Sie, dass dieser Code nur einmal verwendet werden kann und nach 10 Minuten verfällt. Geben Sie den Code nicht an Dritte weiter.\nWenn Sie keinen Bestätigungscode für alex@example.org angefordert haben, können Sie diese E-Mail ignorieren.\n Urheberrecht © 1996–2026 Booking.com. Alle Rechte vorbehalten.\n Diese E-Mail wurde von Booking.com gesendet, Oosterdokskade 163, 1011 DL Amsterdam, Niederlande\nDatenschutz und Cookies <https://www.booking.com/link?t=0f3a9c21> | Kundenservice <https://www.booking.com/link?t=0f3a9c21>",
|
||||
"code": "HUK6QR"
|
||||
},
|
||||
{
|
||||
"sender": "Instagram",
|
||||
"subject": "557 260 is your Instagram recovery code",
|
||||
"preview": "\u00a0\u00a0\u00a0\u00a0Hi alex.muster,\nHere are three easy ways to get back into Instagram.\n\u00a0\u00a0\u00a0\u00a0Log\u00a0in\u00a0without\u00a0password\u00a0\u00a0\u00a0\u00a0Reset\u00a0your\u00a0password\u00a0\u00a0\u00a0\u00a0\u00a0Or use this code in the app:557 260If you didn’t request a login link or a password reset, you can ign...",
|
||||
"body": "Hi alex.muster,\nHere are three easy ways to get back into Instagram.\nLog in without password <https://www.instagram.com/link?t=0f3a9c21>\nReset your password <https://instagram.com/link?t=0f3a9c21>\nOr use this code in the app:\n557 260\nIf you didn’t request a login link or a password reset, you can ignore this message and learn more about why you may have received it <https://help.instagram.com/link?t=0f3a9c21> .\n\nOnly people who know your Instagram password or click the login link in this email can log into your account.\n\n© Instagram. Meta Platforms, Inc., 1601 Willow Road, Menlo Park, CA 94025\nThis message was sent to alex@example.org and intended for alex.muster. Not your profile? Remove your email <https://instagram.com/link?t=0f3a9c21> from this account.",
|
||||
"code": "557260"
|
||||
},
|
||||
{
|
||||
"sender": "STACKIT",
|
||||
"subject": "Verify your email",
|
||||
"preview": "Verify your email. Dear STACKIT customer, To verify your email and activate your account, click to the button below. Verify your email or use the temporary code below Verification code 08MPI6 This code is valid for 1 h. from the time this email was sent...",
|
||||
"body": "Verify your email\nDear STACKIT customer,\nTo verify your email and activate your account, click to the button below.\nVerify your email <https://accounts.stackit.cloud/link?t=0f3a9c21>\nor use the temporary code below\nVerification code\n08MPI6\nThis code is valid for 1 h from the time this email was sent.\nBest regards\nYour STACKIT team\nSTACKIT – EINFACH. SICHER. STABIL.\nSTACKIT GmbH & Co. KG - Stiftsbergstraße 1 D-74172 Neckarsulm\nView our data privacy policy. <https://www.stackit.de/link?t=0f3a9c21>",
|
||||
"code": "08MPI6"
|
||||
},
|
||||
{
|
||||
"sender": "Schramme Media",
|
||||
"subject": "123277 — E-Mail-Adresse bestätigen · Schramme Media & Development",
|
||||
"preview": "Hallo test,\n\nvielen Dank für deine Registrierung bei Schramme Media & Development. Bitte bestätige deine E-Mail-Adresse mit folgendem Code:\n\nCode: 123277\n\nDieser Code ist 15 Minuten gültig. Falls du diese Aktion nicht angefordert hast, ignoriere dies...",
|
||||
"body": "Schramme Media & Development\nE-Mail-Adresse bestätigen\nHallo test,\nvielen Dank für deine Registrierung bei Schramme Media & Development. Bitte bestätige deine E-Mail-Adresse mit folgendem Code:\n 123277\n Dieser Code ist 15 Minuten gültig. Falls du diese Aktion nicht angefordert hast, kannst du diese E-Mail ignorieren.",
|
||||
"code": "123277"
|
||||
},
|
||||
{
|
||||
"sender": "UNiDAYS",
|
||||
"subject": "292326 ist dein UNiDAYS-Passcode",
|
||||
"preview": "EINMALIGER PASSCODE\n\n292326\n\nHier ist dein Passcode.\n\n----------------------------------------\n\nWenn du Hilfe zu deiner UNiDAYS-iD benötigst, wende dich bitte an Support https://www.myunidays.com/link?t=0f3a9c21",
|
||||
"body": "Einmaliger Passcode\n292326\nHier ist dein Passcode.\nWenn du Hilfe zu deiner UNiDAYS-iD benötigst, wende dich bitte an Support <https://www.myunidays.com/link?t=0f3a9c21>",
|
||||
"code": "292326"
|
||||
},
|
||||
{
|
||||
"sender": "Amazon Visa",
|
||||
"subject": "Bestätigungscode",
|
||||
"preview": "<!DOCTYPE html>\n<html lang=\"de\" style=\"width:100%;height:100%;\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n <meta name=\"x-apple-disa...",
|
||||
"body": "Informationen zu deiner Amazon Visa\n\n \u034f\u200c \u034f\u200c \u034f\u200c \u034f\u200c \n \u034f\u200c \u034f\u200c \u034f\u200c \u034f\u200c \n Zinia heißt jetzt Openbank Pay!\n Dein Bestätigungscode lautet 1699\n\n Hallo Alex ,\n\nder von dir angeforderte Code zur Bestätigung des Vorgangs lautet:\n1699\n Das warst nicht du? Überprüfe deine Sicherheitseinstellungen.\n\nViele Grüße\ndein Amazon Visa Kundenservice\nvon Openbank Pay\n\n Wenn du Openbank kontaktieren möchtest, schicke deine Anfrage bitte nicht an diese automatische Adresse, sondern an amazon-visa.de@openbankpay.com\n\nOPEN BANK, S.A. , Av. de Cantabria s/n, 28660 Boadilla del Monte, Spanien\nDu hast noch Fragen? Weitere Infos findest du in den FAQs. <https://www.openbankpay.com/link?t=0f3a9c21> Du hast noch Fragen? Weite",
|
||||
"code": "1699"
|
||||
},
|
||||
{
|
||||
"sender": "Klarna",
|
||||
"subject": "093761 is your 6-digit code",
|
||||
"preview": "093761 is your 6-digit code\n\nhttps://www.klarna.com/link?t=0f3a9c21\n\n\u00a0\n\n\nYour 6-digit code\n\n\u00a0\n\nHi, your 6-digit code is:\n\n\u00a0\n\n093761\n\n\u00a0\n\nUse this code to complete the verification process in the app or website.\n\n\u00a0\n\nDo not share this code. Klarna representatives wi...",
|
||||
"body": "093761 is your 6-digit code\nYour 6-digit code\nHi, your 6-digit code is:\n093761\n Use this code to complete the verification process in the app or website.\nDo not share this code. Klarna representatives will never reach out to you to verify this code over the phone or SMS.\nThe code is valid for 10 minutes.\n Fraudsters may pretend to be Klarna. Never share login details or codes.\nDidn't request a verification code?\n If you suspect someone else requested this code, lock your account <https://klarna.com/link?t=0f3a9c21> immediately.\nHave a question?\nGet 24/7 support and find answers to your questions with our customer service assistant.\nChat with us <https://app.klarna.com/link?t=0f3a9c21>\nKlarn",
|
||||
"code": "093761"
|
||||
},
|
||||
{
|
||||
"sender": "KI Palooza",
|
||||
"subject": "Your Login Passcode for KI Palooza 2026",
|
||||
"preview": " \u00a0 \u00a0Hey a a!\nWillkommen zur KI Palooza 2026\nDein Passcode\n551471\nKI Palooza 2026 Sep 29 2026 - Sep 30 2026\nHallo a a,\n\n\nWir schreiben dir weil wir eine Anfrage zur Anmeldung zu deinem Konto bekommen haben. Verwende den untenstehenden Passcode um dich ...",
|
||||
"body": "Hey a a!\nWillkommen zur KI Palooza 2026\nDein Passcode\n551471\nKI Palooza 2026\nSep 29 2026 - Sep 30 2026\nHallo a a,\n\nWir schreiben dir weil wir eine Anfrage zur Anmeldung zu deinem Konto bekommen haben. Verwende den untenstehenden Passcode um dich in der App einzuloggen.\nDein Passcode ist\n551471\nDein QR Code\n\nPrüfe dein Profil und lege los!\nZu deinem Profil <https://kipalooza2025.eventify.io/link?t=0f3a9c21>\nDownload KI Palooza 2026 Mobile App for\niOS and Android\nMake sure you download KI Palooza 2026 app. Easily connect with other attendees, view event schedules, speakers, get news updates, bookmark your favourites, and post on social feed.\nWillkommen zur KI Palooza 2026\nDein Passcode : 55147",
|
||||
"code": "551471"
|
||||
},
|
||||
{
|
||||
"sender": "X",
|
||||
"subject": "Your X confirmation code is 5aem5f5z",
|
||||
"preview": "X\n\nWe noticed an attempt to log in to your account @alexm that seems suspicious. Was this you?\n\n\n\nFalls du das warst\nJust to be safe, to log in to this account you’ll need to confirm this is really you by entering the following single-use code.\n\n5...",
|
||||
"body": "We noticed an attempt to log in to your account @alexm that seems suspicious. Was this you?\nFalls du das warst\n Just to be safe, to log in to this account you’ll need to confirm this is really you by entering the following single-use code.\n5aem5f5z\nFalls du das nicht warst\n Complete these steps now to protect your account.\nChange your password. <https://t.co/link?t=0f3a9c21> You'll be logged out of all your active X sessions except the one you're using at this time.\nReview the apps <https://t.co/link?t=0f3a9c21> that have access to your account and revoke access to any unfamiliar apps. Learn more <https://help.x.com/link?t=0f3a9c21> .\nHilfe <https://t.co/link?t=0f3a9c21> | Nicht mein Account <https://twitter.com/link?t=0f3a9c21",
|
||||
"code": "5aem5f5z"
|
||||
},
|
||||
{
|
||||
"sender": "Vaultwarden",
|
||||
"subject": "Vaultwarden Login Verification Code",
|
||||
"preview": "Your two-step verification code is: 7030417738\n\nUse this code to complete logging in with Vaultwarden.\n\n===\nGithub: https://github.com/link?t=0f3a9c21",
|
||||
"body": "Your two-step verification code is: 7030417738\n Use this code to complete logging in with Vaultwarden.",
|
||||
"code": "7030417738"
|
||||
},
|
||||
{
|
||||
"sender": "fonial",
|
||||
"subject": "PIN-Verifizierung",
|
||||
"preview": "\nGuten Tag Alex Muster,\nhiermit erhalten Sie Ihre persönliche PIN zur Verifizierung des Kundenkonto-Logins:\n 615233\nDiese PIN ist 10 Minuten gültig und läuft um 19:13 Uhr ab.\nBitte beachten Sie, dass der Link nur auf dem Gerät funktioniert, dass Sie ...",
|
||||
"body": "Guten Tag Alex Muster,\nhiermit erhalten Sie Ihre persönliche PIN zur Verifizierung des Kundenkonto-Logins:\n615233 <https://kundenkonto.fonial.de/link?t=0f3a9c21>\nDiese PIN ist 10 Minuten gültig und läuft um 19:13 Uhr ab.\n Bitte beachten Sie, dass der Link nur auf dem Gerät funktioniert, dass Sie zum Login benutzt haben. Lesen Sie diese E-Mail auf einem anderen Gerät, z. B. Ihrem Smartphone? Geben Sie die PIN bitte von Hand in das Formularfeld ein, anstatt auf den Link zu klicken.\nGeben Sie diese PIN niemals weiter!\n Mitarbeiter von fonial fragen niemals nach dieser PIN. Teilen Sie diese daher nie mit fonial oder Dritten – es könnte sich um einen Versuch handeln unberechtigt in Ihr Kundenkont",
|
||||
"code": "615233"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,225 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
findVerificationCode,
|
||||
htmlMayNameACode,
|
||||
listVerificationCode,
|
||||
verificationCodeBodyText,
|
||||
VERIFICATION_CODE_MAX_AGE_MS,
|
||||
} from '../verification-code';
|
||||
// Real sign-in and confirmation mails as Stalwart 0.16.23 serves them: the
|
||||
// list's subject and preview, and the body text the reader derives from the
|
||||
// HTML part. Names, addresses and link tokens are replaced.
|
||||
import mails from './fixtures/verification-code-mails.json';
|
||||
|
||||
// Mail whose code is neither in the subject nor in the 256-character
|
||||
// preview: its body opens with CSS, raw HTML or padding, or the preview
|
||||
// ends before (or in the middle of) the code.
|
||||
const NOT_IN_PREVIEW = new Set([
|
||||
'Lieferando', 'Planauskunft', 'Tuya', 'Telekom Geschäftskunden', 'Telekom Login', 'Steam', 'Autodesk',
|
||||
'Amazon Visa',
|
||||
]);
|
||||
|
||||
describe('findVerificationCode on real mail', () => {
|
||||
it.each(mails.map((m) => [m.sender, m] as const))('finds the code of %s in the reader', (_, mail) => {
|
||||
expect(findVerificationCode(mail.subject, mail.body)).toBe(mail.code);
|
||||
});
|
||||
|
||||
it.each(mails.map((m) => [m.sender, m] as const))('finds the code of %s in the list, or nothing', (sender, mail) => {
|
||||
expect(findVerificationCode(mail.subject, mail.preview)).toBe(NOT_IN_PREVIEW.has(sender) ? null : mail.code);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findVerificationCode', () => {
|
||||
it('copies grouped digits without the gaps', () => {
|
||||
expect(findVerificationCode('557 260 is your Instagram recovery code', '')).toBe('557260');
|
||||
expect(findVerificationCode('', 'Bitte gebe folgenden Bestätigungs-Code ein: 3 0 4 6 2 7 Gebe diesen')).toBe('304627');
|
||||
expect(findVerificationCode('', 'Your code: 123-456')).toBe('123456');
|
||||
});
|
||||
|
||||
it('keeps the hyphen of a lettered code', () => {
|
||||
expect(findVerificationCode('Slack confirmation code: 6ZA-BRE', '')).toBe('6ZA-BRE');
|
||||
});
|
||||
|
||||
it('does not offer a code the preview cut in half', () => {
|
||||
expect(findVerificationCode('', 'Bitte verwenden Sie den folgenden Steam-Guard-Code:\n\nAnmeldecode\nY9DG...')).toBeNull();
|
||||
expect(findVerificationCode('', 'Your code is 1234...')).toBeNull();
|
||||
});
|
||||
|
||||
it('finds digits run into the words around them', () => {
|
||||
expect(findVerificationCode('', 'Gib diesen Code ein:86771674Teile diesen Code mit niemandem.')).toBe('86771674');
|
||||
expect(findVerificationCode('', 'Sign in to Claude.ai177945Copy and paste the temporary verification code')).toBe('177945');
|
||||
});
|
||||
|
||||
it('does not take a group of digits out of an id', () => {
|
||||
expect(findVerificationCode('', 'Your verification code was sent. Request ffacd0b43b97d-4887a30a5a2si2098f4f')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not read a status code as the keyword of a one-time code', () => {
|
||||
expect(findVerificationCode('', 'Rejected with code 550 (5.1.1) after 48291733 ms')).toBeNull();
|
||||
expect(findVerificationCode('', 'Antwort mit Code 421: Server ausgelastet, Sitzung 48291733 beendet')).toBeNull();
|
||||
});
|
||||
|
||||
it('prefers the code in the subject', () => {
|
||||
expect(findVerificationCode('Your code is 482913', 'Your verification code for account 5512345 is below')).toBe('482913');
|
||||
});
|
||||
|
||||
it('takes the code the keyword introduces over other numbers near it', () => {
|
||||
expect(findVerificationCode('', 'Your verification code for account 12345678 is 482913.')).toBe('482913');
|
||||
expect(findVerificationCode('', 'Dein Passcode\n551471\nKI Palooza 2026 Sep 29 2026 - Sep 30 2026')).toBe('551471');
|
||||
});
|
||||
|
||||
it('accepts a year-shaped code only when it is presented as the code', () => {
|
||||
expect(findVerificationCode('', 'Your Login Passcode for KI Palooza 2026')).toBeNull();
|
||||
expect(findVerificationCode('', 'Your code is 2013')).toBe('2013');
|
||||
});
|
||||
|
||||
it('reads keywords in other languages', () => {
|
||||
expect(findVerificationCode('', 'Tu código de verificación es 482913')).toBe('482913');
|
||||
expect(findVerificationCode('', 'Twój kod weryfikacyjny: 482913')).toBe('482913');
|
||||
expect(findVerificationCode('', 'Ваш код подтверждения: 482913')).toBe('482913');
|
||||
expect(findVerificationCode('', '您的验证码是 482913')).toBe('482913');
|
||||
expect(findVerificationCode('', 'Din engangskode er 482913')).toBe('482913');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an order confirmation', 'Deine Bestellung Nr. 302-4829175-1937264',
|
||||
'Vielen Dank für deine Bestellung vom 26.09.2026. Bestellnummer: 302-4829175-1937264 Summe: 49,99 € Deine Kundennummer: 5512345.'],
|
||||
['a shipping notice with a tracking code', 'Ihr Paket ist unterwegs',
|
||||
'Sendungsnummer 00340434161234567890. Mit dem Tracking-Code JJD0003900012 können Sie Ihre Sendung verfolgen. Zustellung am 28.09.2026 zwischen 10:00 und 14:00 Uhr.'],
|
||||
['a promotion', 'Nur heute: 25 % auf alles',
|
||||
'Mit dem Gutscheincode HERBST25 sparst du 25 %. Or use code THANKS10 for 10% off. Use promo code FALL2026 at checkout. © 2026 Shop GmbH, 10115 Berlin'],
|
||||
['a code review notification', 'Re: [bulwarkmail/webmail] Show verification codes (PR #1043)',
|
||||
'The code at line 1042 should use the shared helper, see commit 5f3e2a1 and issue #1039. Claude Code 2.1.14 fixed it.'],
|
||||
['an invoice', 'Ihre Rechnung RE-2026-10432',
|
||||
'Rechnungsbetrag 119,00 EUR. Bitte überweisen Sie bis 10.10.2026 auf IBAN DE89 3704 0044 0532 0130 00, BIC COBADEFFXXX.'],
|
||||
['a sign-in alert without a code', 'Neue Anmeldung bei deinem Konto',
|
||||
'Neue Anmeldung von Windows 11 um 14:32 Uhr in Berlin. Du musst keinen Code eingeben. Rufen Sie uns an unter +49 800 3301000 oder 0800 330 1000.'],
|
||||
['a code of conduct update', 'Updates to our Code of Conduct',
|
||||
'We updated our Code of Conduct effective 1 October 2026. Contact us at 1 Market St, San Francisco, CA 94105.'],
|
||||
['a card notice', 'Ihre Kreditkarte',
|
||||
'Geben Sie niemals Ihren Sicherheitscode oder Ihre PIN weiter. Ihre Karte endet auf 4821. Meeting-ID: 845 1234 5678'],
|
||||
['a QR code', 'Deine Mitgliedskarte', 'Scanne den QR-Code in der App. Deine Mitgliedsnummer 20394857.'],
|
||||
['a kernel log with an oops dump', 'Kernel warnings on host',
|
||||
'kern :warn : [Tue Oct 13 15:37:13 2026] 2026-10-13T15:37:13,834096-04:00 Code: 48 8b 45 00 0f 0b\nkern :warn : [Tue Oct 13 15:37:13 2026] 2026-10-13T15:37:13,834096-04:00 Tainted: [P]=PROPRIETARY_MODULE, [W]=WARN, [O]=OOT_MODULE'],
|
||||
['a code expiry timestamp', 'Your access code', 'Your code expires 2026-10-13T15:37:13Z.'],
|
||||
['a delivery report from Gmail', 'Successfully delivered message',
|
||||
"Your message has been successfully delivered to the following recipients:\n\n<x@gmail.com> (delivered to 'gmail-smtp-in.l.google.com' with code 250 (2.1.5) 'OK ffacd0b43b97d-4887a30a5a2si20984394f4f.30 - gsmtp')"],
|
||||
['a delivery report from Outlook', 'Successfully delivered message',
|
||||
"<x@example.com> (delivered to 'example-com.mail.protection.outlook.com' with code 250 (2.6.0) '2.6.0 <a1b2@mail.example.org> [InternalId=21045339521540, Hostname=PAXPR03MB8065.eurprd03.prod.outlook.com] 12825 bytes in 0.108, 115.431 KB/sec Queued mail for delivery')"],
|
||||
])('finds nothing in %s', (_, subject, text) => {
|
||||
expect(findVerificationCode(subject, text)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listVerificationCode', () => {
|
||||
const now = Date.parse('2026-09-27T12:00:00Z');
|
||||
const row = (receivedAt: string) => ({ subject: 'Your code is 482913', preview: '', receivedAt });
|
||||
|
||||
it('shows the code of a fresh mail', () => {
|
||||
expect(listVerificationCode(row('2026-09-27T11:58:00Z'), now)).toBe('482913');
|
||||
});
|
||||
|
||||
it('skips mail older than a day', () => {
|
||||
expect(listVerificationCode(row(new Date(now - VERIFICATION_CODE_MAX_AGE_MS - 1000).toISOString()), now)).toBeNull();
|
||||
});
|
||||
|
||||
it('allows for a server clock a little ahead', () => {
|
||||
expect(listVerificationCode(row('2026-09-27T12:02:00Z'), now)).toBe('482913');
|
||||
expect(listVerificationCode(row('2026-09-28T12:00:00Z'), now)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verificationCodeBodyText', () => {
|
||||
const part = (partId: string, type: string) => ({ partId, blobId: `blob-${partId}`, size: 1, type });
|
||||
const value = (v: string) => ({ value: v, isEncodingProblem: false, isTruncated: false });
|
||||
|
||||
it('reads the HTML part as the reader shows it, without its stylesheet', () => {
|
||||
const text = verificationCodeBodyText({
|
||||
htmlBody: [part('2', 'text/html')],
|
||||
textBody: [part('1', 'text/plain')],
|
||||
bodyValues: {
|
||||
1: value('This email is only available as HTML.'),
|
||||
2: value('<html><body><style>.x{width:600px;color:#123456}</style><table><tr><td>Verification code:</td></tr><tr><td><b>261765</b></td></tr></table></body></html>'),
|
||||
},
|
||||
});
|
||||
expect(text).not.toContain('600px');
|
||||
expect(findVerificationCode('Verification Code', text)).toBe('261765');
|
||||
});
|
||||
|
||||
// Opening a big HTML mail spent most of its script time turning the HTML
|
||||
// into text to look for a code. Without any word that names a code there is
|
||||
// nothing to find, so that step is skipped; it must never skip a mail that
|
||||
// has one, however the word is written.
|
||||
const html = (body: string) => verificationCodeBodyText({
|
||||
htmlBody: [part('1', 'text/html')],
|
||||
textBody: [],
|
||||
bodyValues: { 1: value(`<html><body>${body}</body></html>`) },
|
||||
});
|
||||
|
||||
it('skips reading HTML that names no code', () => {
|
||||
const newsletter = '<h2>Vacation policy update #998</h2><table><tr><td>row-1</td><td>0.16487150</td></tr></table><p>Lorem ipsum 482910 commodo</p>';
|
||||
expect(htmlMayNameACode(newsletter)).toBe(false);
|
||||
expect(html(newsletter)).toBe('');
|
||||
});
|
||||
|
||||
it('still reads HTML whose code word is marked up, escaped or padded', () => {
|
||||
expect(findVerificationCode('', html('Your verification <b>code</b> is 482910'))).toBe('482910');
|
||||
expect(findVerificationCode('', html('Tu código de verificación es 482910'))).toBe('482910');
|
||||
expect(findVerificationCode('', html('Ваш код: 482910'))).toBe('482910');
|
||||
expect(findVerificationCode('', html('Your verification co­de is 482910'))).toBe('482910');
|
||||
expect(findVerificationCode('', html('Ihre mTAN lautet 482910'))).toBe('482910');
|
||||
});
|
||||
|
||||
it('reads plain-text mail', () => {
|
||||
const text = verificationCodeBodyText({
|
||||
htmlBody: [part('1', 'text/plain')],
|
||||
textBody: [part('1', 'text/plain')],
|
||||
bodyValues: { 1: value('Your HA Remote verification code is 811083.') },
|
||||
});
|
||||
expect(findVerificationCode('', text)).toBe('811083');
|
||||
});
|
||||
});
|
||||
|
||||
// A sender's bytes drive these regexes on the UI thread, for every opened
|
||||
// mail and every list row. Each of these inputs took seconds to minutes
|
||||
// before the patterns were anchored; all must stay linear.
|
||||
describe('crafted mail cannot stall the code search', () => {
|
||||
const BUDGET_MS = 250;
|
||||
const part = (partId: string, type: string) => ({ partId, blobId: `blob-${partId}`, size: 1, type });
|
||||
const value = (v: string) => ({ value: v, isEncodingProblem: false, isTruncated: false });
|
||||
function timed(fn: () => unknown): number {
|
||||
const started = performance.now();
|
||||
fn();
|
||||
return performance.now() - started;
|
||||
}
|
||||
const htmlMail = (body: string) => ({
|
||||
htmlBody: [part('1', 'text/html')],
|
||||
textBody: [],
|
||||
bodyValues: { 1: value(body) },
|
||||
});
|
||||
|
||||
it('a megabyte of unclosed <style> tags', () => {
|
||||
const mail = htmlMail('<style>'.repeat(150_000));
|
||||
expect(timed(() => verificationCodeBodyText(mail))).toBeLessThan(BUDGET_MS);
|
||||
});
|
||||
|
||||
it('a body of < without >', () => {
|
||||
const mail = htmlMail('code '.concat('<'.repeat(200_000)));
|
||||
expect(timed(() => verificationCodeBodyText(mail))).toBeLessThan(BUDGET_MS);
|
||||
expect(timed(() => htmlMayNameACode('<'.repeat(200_000)))).toBeLessThan(BUDGET_MS);
|
||||
});
|
||||
|
||||
it('one long word, glued digit groups, and a long subject', () => {
|
||||
expect(timed(() => findVerificationCode('', 'code ' + 'a'.repeat(50_000)))).toBeLessThan(BUDGET_MS);
|
||||
expect(timed(() => findVerificationCode('', 'code ' + 'a1234'.repeat(10_000)))).toBeLessThan(BUDGET_MS);
|
||||
expect(timed(() => findVerificationCode('code ' + 'a'.repeat(200_000), ''))).toBeLessThan(BUDGET_MS);
|
||||
expect(timed(() => findVerificationCode('', 'code ' + 'a'.repeat(20_000) + '...'))).toBeLessThan(BUDGET_MS);
|
||||
});
|
||||
|
||||
it('still finds the code after the trims', () => {
|
||||
expect(findVerificationCode('Your code is 177945 ' + 'x'.repeat(2_000), '')).toBe('177945');
|
||||
expect(findVerificationCode('', 'Claude.ai177945Copy this code')).toBe('177945');
|
||||
expect(findVerificationCode('', 'Your code: 482910 reply to noreply@example.com')).toBe('482910');
|
||||
expect(findVerificationCode('', 'Your code: 48291...')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,412 @@
|
||||
// Port of the webmail's lib/verification-code.ts (3ed484df and later).
|
||||
import type { Email } from '../api/types';
|
||||
import { pickEmailBody, selectRenderableHtml } from './email-body';
|
||||
|
||||
/**
|
||||
* Finds the one-time code in a sign-in or confirmation mail ("Your
|
||||
* verification code is 177945", "HUK6QR ist Ihr Bestätigungscode"), so the
|
||||
* list and the reader can offer it for copying.
|
||||
*
|
||||
* A token only counts when it looks like a code and sits near a word that
|
||||
* names one (code, Bestätigungscode, Passcode, PIN, OTP, ...). Mail is full
|
||||
* of numbers — dates, prices, postcodes, order and phone numbers — and a
|
||||
* wrong chip is worse than none, so anything doubtful is left out.
|
||||
*/
|
||||
|
||||
// A code this far after its keyword ("Der Bestätigungscode ist unten
|
||||
// aufgeführt. Bitte geben Sie ihn ... ein. 37CCHC") still counts, one this far
|
||||
// before it ("548980 ist Ihr Bestätigungscode") too.
|
||||
const MAX_AFTER = 120;
|
||||
const MAX_BEFORE = 60;
|
||||
|
||||
// Words that end in a code keyword but name something else: "Gutscheincode",
|
||||
// "Postcode", "Barcode", and the word before a bare "code" ("promo code",
|
||||
// "Claude Code", "VS Code").
|
||||
const NOT_A_CODE_PREFIXES = new Set([
|
||||
'en', 'de', 'uni', 'x', 'vs', 'op', 'byte', 'bar', 'post', 'zip', 'qr', 'geo', 'short', 'pseudo',
|
||||
'hash', 'leet', 'trans', 're', 'source', 'quell', 'programm', 'program', 'status', 'error',
|
||||
'fehler', 'dress', 'color', 'colour', 'farb', 'html', 'css', 'embed', 'tracking', 'sendungs',
|
||||
'referral', 'empfehlungs', 'sort', 'swift', 'tax', 'ean', 'produkt', 'product', 'artikel',
|
||||
'promo', 'coupon', 'discount', 'voucher', 'gift', 'gutschein', 'rabatt', 'aktions', 'geschenk',
|
||||
'bonus', 'vorteils', 'area', 'country', 'länder', 'laender', 'landes', 'claude', 'studio',
|
||||
]);
|
||||
|
||||
// Word endings that name a code, across the languages the app ships.
|
||||
// "bestätigungscode", "verificatiecode", "engangskode", "vahvistuskoodi",
|
||||
// "verifieringskod", "ellenőrzőkód", "код", ...
|
||||
const CODE_WORD_ENDINGS = [
|
||||
'code', 'codes', 'codice', 'codici', 'código', 'códigos', 'codigo', 'codigos', 'cod', 'kod',
|
||||
'kodu', 'kodem', 'kodunuz', 'kód', 'kódot', 'kódu', 'kode', 'koden', 'koodi', 'koodia', 'kood',
|
||||
'kods', 'kodas', 'код', 'кода', 'коду', 'кодом', 'κωδικός', 'κωδικό', 'קוד', 'رمز', 'کد',
|
||||
];
|
||||
|
||||
const CODE_WORDS_EXACT = new Set(['passcode', 'einmalkennwort', 'einmalpasswort']);
|
||||
|
||||
// Only ever written in capitals when they mean a code; "pin" and "tan" are
|
||||
// ordinary words too.
|
||||
const UPPERCASE_CODE_WORDS = /^(?:PIN|OTP|(?:m|sms|push|chip|photo|i|e)?TAN)$/;
|
||||
|
||||
// Scripts without spaces between words, matched as substrings.
|
||||
const CJK_CODE_WORDS = /コード|認証番号|確認番号|验证码|驗證碼|校验码|动态码|인증\s?번호|인증\s?코드|코드/g;
|
||||
|
||||
const ONE_TIME_PASSWORD = /\b(?:one[- ]time|single[- ]use|temporary) pass(?:word|code)\b/gi;
|
||||
|
||||
const WORD = /[\p{L}\p{M}]+(?:-[\p{L}\p{M}]+)*/gu;
|
||||
|
||||
// "with code 250 (2.1.5)" in a delivery report: a code word followed by a
|
||||
// number too short to be a one-time code names a status or error code.
|
||||
const LABELS_SHORT_NUMBER = /^\s*[:=]?\s*\d{1,3}(?![\p{L}\p{N}]|[.,]\d|[ -]\d)/u;
|
||||
|
||||
function isCodeWordPart(part: string, previous: string | undefined): boolean {
|
||||
if (UPPERCASE_CODE_WORDS.test(part)) return true;
|
||||
const lower = part.toLowerCase();
|
||||
if (CODE_WORDS_EXACT.has(lower)) return true;
|
||||
for (const ending of CODE_WORD_ENDINGS) {
|
||||
if (!lower.endsWith(ending)) continue;
|
||||
const prefix = lower.slice(0, lower.length - ending.length);
|
||||
// A bare "code" takes its meaning from the word before it.
|
||||
const qualifier = prefix || previous?.toLowerCase();
|
||||
return !qualifier || !NOT_A_CODE_PREFIXES.has(qualifier);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface Span { start: number; end: number }
|
||||
|
||||
function findCodeWords(text: string): Span[] {
|
||||
const spans: Span[] = [];
|
||||
let previousWord: string | undefined;
|
||||
for (const match of text.matchAll(WORD)) {
|
||||
const start = match.index!;
|
||||
const end = start + match[0].length;
|
||||
const parts = match[0].split('-');
|
||||
const hit = parts.some((part, i) => isCodeWordPart(part, i > 0 ? parts[i - 1] : previousWord));
|
||||
const next = text.slice(end, end + 12);
|
||||
// "Code of Conduct", "code of practice"
|
||||
if (hit && !/^\s+of\s/i.test(next) && !LABELS_SHORT_NUMBER.test(next)) spans.push({ start, end });
|
||||
previousWord = parts[parts.length - 1];
|
||||
}
|
||||
for (const re of [CJK_CODE_WORDS, ONE_TIME_PASSWORD]) {
|
||||
for (const match of text.matchAll(re)) {
|
||||
spans.push({ start: match.index!, end: match.index! + match[0].length });
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
// Tried in this order at each position, so "557 260" and "6ZA-BRE" are taken
|
||||
// whole before their pieces are.
|
||||
const CANDIDATE = new RegExp(
|
||||
[
|
||||
// 6ZA-BRE, NJF63-SW7S7, and 2025-12-12 (dropped later)
|
||||
String.raw`(?<![\p{L}\p{N}])[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{2,8}){1,3}(?![\p{L}\p{N}])`,
|
||||
// 557 260, 123-456
|
||||
String.raw`(?<!\d)\d{3,4}[ -]\d{3,4}(?!\d)`,
|
||||
// 3 0 4 6 2 7
|
||||
String.raw`(?<![\p{L}\p{N}])\d(?: \d){3,9}(?![\p{L}\p{N}])`,
|
||||
// 37CCHC, 5aem5f5z, 177945
|
||||
String.raw`(?<![\p{L}\p{N}])[A-Za-z0-9]{4,10}(?![\p{L}\p{N}])`,
|
||||
].join('|'),
|
||||
'gu',
|
||||
);
|
||||
|
||||
// Digits run into the words around them, which the pass above takes as one
|
||||
// token: "ein:86771674Teile", "Claude.ai177945Copy", "732888Gib".
|
||||
const GLUED_DIGITS = /(?<=\p{L})\d{4,10}(?!\d)|(?<!\d)\d{4,10}(?=\p{L})/gu;
|
||||
|
||||
const ALNUM = /[\p{L}\p{N}]/u;
|
||||
|
||||
/** Whether the digits at start..end have only letters around them, as in
|
||||
* "ai177945Copy", and are not one group of an id like "4887a30a5a2si2098"
|
||||
* or "PAXPR03MB8065". */
|
||||
function gluedToWords(text: string, start: number, end: number): boolean {
|
||||
// A longer run is never word + code + word; walking it whole for every
|
||||
// digit group in it would make one long token cost quadratic time.
|
||||
const floor = Math.max(0, start - GLUED_CONTEXT);
|
||||
const ceiling = Math.min(text.length, end + GLUED_CONTEXT);
|
||||
while (start > floor && ALNUM.test(text[start - 1])) start--;
|
||||
while (end < ceiling && ALNUM.test(text[end])) end++;
|
||||
if ((start > 0 && ALNUM.test(text[start - 1])) || (end < text.length && ALNUM.test(text[end]))) return false;
|
||||
return /^\p{L}*\d+\p{L}*$/u.test(text.slice(start, end));
|
||||
}
|
||||
const GLUED_CONTEXT = 40;
|
||||
|
||||
const YEAR = /^(?:19|20)\d\d$/;
|
||||
// 10min, 600px, 100GB
|
||||
const NUMBER_WITH_UNIT = /^\d+[a-z]{1,3}$/i;
|
||||
// THANKS10, HERBST25, FALL2026
|
||||
const PROMO_CODE = /^[A-Z]{4,}\d{1,4}$/;
|
||||
const PROMO_WORDS = /%|(?<!\p{L})(?:off|rabatt|spar\p{L}*|save|discount|coupon|gutschein\p{L}*|voucher|sale|angebot\p{L}*)(?!\p{L})/iu;
|
||||
// A number labelled as something else: "Kundennummer: 5512345", "Meeting-ID:
|
||||
// 845 1234 5678", "at line 1042", "card ending in 4821". Nouns that also
|
||||
// label codes ("Code für dein Konto: 123456") count only without a colon.
|
||||
const LABELLED_AS_OTHER = new RegExp(
|
||||
String.raw`(?:(?:\p{L}*(?:nummer|number)|(?<!\p{L})(?:nr|no|id|line|zeile|version|tel|telefon|phone|fax|mobil|plz))\.?:?` +
|
||||
String.raw`|(?<!\p{L})(?:order|bestellung|account|konto|invoice|rechnung|ticket|case|page|seite|room|zimmer|commit|issue|pr|build|run|job|step|port)` +
|
||||
String.raw`|ending in|endet auf|endend auf|ends with)[ #]*$`,
|
||||
'iu',
|
||||
);
|
||||
const LOWERCASE_HEX = /^[0-9a-f-]+$/;
|
||||
// "The code is: 123456", "Ihr Code lautet 123456"
|
||||
const LINKED_BY = /(?::|=|(?<!\p{L})(?:is|lautet|ist|est|es|è|é|är|er|jest|je))\s*$/iu;
|
||||
|
||||
const LETTER = /\p{L}/u;
|
||||
|
||||
/** Text around a candidate, not starting or ending inside a word, so that
|
||||
* "off" cut from "offiziellen" does not read as a word of its own. */
|
||||
function contextWindow(text: string, from: number, to: number): string {
|
||||
let start = Math.max(0, from);
|
||||
let end = Math.min(text.length, to);
|
||||
while (start > 0 && start < end && LETTER.test(text[start - 1]) && LETTER.test(text[start])) start++;
|
||||
while (end < text.length && end > start && LETTER.test(text[end]) && LETTER.test(text[end - 1])) end--;
|
||||
return text.slice(start, end);
|
||||
}
|
||||
|
||||
interface Candidate extends Span { code: string; score: number }
|
||||
|
||||
function scoreCandidate(text: string, raw: string, start: number, keywords: Span[]): Candidate | null {
|
||||
const end = start + raw.length;
|
||||
const letters = raw.replace(/[^A-Za-z]/g, '');
|
||||
const digits = raw.replace(/\D/g, '');
|
||||
const before = contextWindow(text, start - 40, start);
|
||||
const after = text.slice(end, end + 8);
|
||||
|
||||
if (!digits) return null;
|
||||
if (letters && letters !== letters.toUpperCase() && letters !== letters.toLowerCase()) return null;
|
||||
|
||||
let code = raw;
|
||||
if (!letters) {
|
||||
// Grouped digits are typed without the gaps: "557 260" is 557260.
|
||||
code = digits;
|
||||
if (raw.includes('-') && !/^\d{3,4}-\d{3,4}$/.test(raw)) return null; // dates, phone numbers
|
||||
if (digits.length < 4 || digits.length > 10) return null;
|
||||
// Part of a date, time, decimal or longer number: 12.12.2025, 10:49:27, 1.299,00
|
||||
if (/\d[.,:/-]$/.test(before) || /^[.,:/-]\d/.test(after)) return null;
|
||||
// Issue and ticket numbers, phone numbers, the next group of an IBAN
|
||||
if (/[#+]$/.test(before) || /(?:\d|\))[ /-]?$/.test(before) || /^[ /-]?\d/.test(after)) return null;
|
||||
// Amounts
|
||||
if (/[€$£]\s?$/.test(before) || /^\s?(?:[€$£%]|eur\b|usd\b|chf\b)/i.test(after)) return null;
|
||||
} else if (NUMBER_WITH_UNIT.test(raw) || PROMO_CODE.test(raw)) {
|
||||
return null;
|
||||
} else if (/^\d{4}-\d\d-\d\dT\d/i.test(raw)) {
|
||||
// An ISO timestamp cut at its first colon: 2026-10-13T15:37:13
|
||||
return null;
|
||||
}
|
||||
if (LABELLED_AS_OTHER.test(before)) return null;
|
||||
if (PROMO_WORDS.test(contextWindow(text, start - 60, end + 40))) return null;
|
||||
|
||||
let best = Infinity;
|
||||
let bestAfter = Infinity;
|
||||
for (const kw of keywords) {
|
||||
if (kw.start < end && kw.end > start) return null; // the candidate is part of a keyword ("2FA-Code")
|
||||
if (kw.end <= start && start - kw.end <= MAX_AFTER) {
|
||||
bestAfter = Math.min(bestAfter, start - kw.end);
|
||||
best = Math.min(best, start - kw.end);
|
||||
}
|
||||
if (kw.start >= end && kw.start - end <= MAX_BEFORE) best = Math.min(best, (kw.start - end) * 1.5);
|
||||
}
|
||||
if (best === Infinity) return null;
|
||||
|
||||
// Lowercase letters also make up words, hashes and ids, so they have to
|
||||
// follow their keyword closely ("confirmation code is 5aem5f5z"), and a
|
||||
// lowercase hex string (a commit, a colour, a token) only counts when the
|
||||
// keyword labels it directly ("den Code e382e507-1593-47f8").
|
||||
if (letters && letters === letters.toLowerCase()) {
|
||||
if (bestAfter > 40) return null;
|
||||
if (LOWERCASE_HEX.test(raw) && bestAfter > 2) return null;
|
||||
}
|
||||
|
||||
const linked = LINKED_BY.test(before);
|
||||
const ownLine = /(?:^|\n)\s*$/.test(before) && /^\s*(?:\n|$)/.test(after);
|
||||
// "KI Palooza 2026": a year is only the code when it is presented as one.
|
||||
if (!letters && YEAR.test(digits) && !linked && !ownLine) return null;
|
||||
|
||||
let score = best;
|
||||
if (linked) score -= 15;
|
||||
if (ownLine) score -= 15;
|
||||
return { start, end, code, score };
|
||||
}
|
||||
|
||||
function normalize(text: string): string {
|
||||
return text
|
||||
// Invisible spacers senders put between characters
|
||||
.replace(/[\u00ad\u200b-\u200f\u2060\ufeff]|\u034f/g, '')
|
||||
// Links and addresses carry ids and tokens, never the code to type.
|
||||
// Anchored to the start of a token so a long run of letters is scanned
|
||||
// once, not once per character.
|
||||
.replace(/https?:\/\/\S+|www\.\S+/gi, ' ')
|
||||
.replace(/(?<!\S)\S*@\S+/g, ' ')
|
||||
// Keep line breaks (a code often stands on a line of its own) but not
|
||||
// the runs of blank lines and indentation between table cells.
|
||||
.replace(/[^\S\n]+/g, ' ')
|
||||
.replace(/ ?\n[\s]*/g, '\n')
|
||||
// A preview is cut off with "...", and the word it cut may be half a code.
|
||||
.replace(/(?<![\p{L}\p{N}-])[\p{L}\p{N}-]*(?:\.\.\.|…)\s*$/u, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// A subject is a line, not a document; the MTA's message size is the only
|
||||
// bound on its length, and the list runs this for every row.
|
||||
const MAX_SUBJECT = 1_000;
|
||||
|
||||
function bestCandidate(rawText: string): Candidate | null {
|
||||
const text = normalize(rawText);
|
||||
if (!text) return null;
|
||||
const keywords = findCodeWords(text);
|
||||
if (!keywords.length) return null;
|
||||
|
||||
let best: Candidate | null = null;
|
||||
for (const re of [CANDIDATE, GLUED_DIGITS]) {
|
||||
for (const match of text.matchAll(re)) {
|
||||
if (re === GLUED_DIGITS && !gluedToWords(text, match.index!, match.index! + match[0].length)) continue;
|
||||
const candidate = scoreCandidate(text, match[0], match.index!, keywords);
|
||||
if (candidate && (!best || candidate.score < best.score)) best = candidate;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one-time code in a mail, or null. `text` is the preview for a list
|
||||
* row and the body for the reader. A code in the subject wins, since
|
||||
* senders that put it there ("Your code is 177945") also tend to repeat
|
||||
* it next to unrelated numbers in the body.
|
||||
*/
|
||||
export function findVerificationCode(subject: string | null | undefined, text: string | null | undefined): string | null {
|
||||
return bestCandidate((subject ?? '').slice(0, MAX_SUBJECT))?.code
|
||||
?? bestCandidate((text ?? '').slice(0, MAX_BODY_TEXT))?.code
|
||||
?? null;
|
||||
}
|
||||
|
||||
/** Only fresh codes are worth a chip in the list; they expire within minutes to a day. */
|
||||
export const VERIFICATION_CODE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function isFreshForVerificationCode(receivedAt: string | undefined, now = Date.now()): boolean {
|
||||
if (!receivedAt) return false;
|
||||
const age = now - new Date(receivedAt).getTime();
|
||||
// A little slack for a server clock ahead of this one.
|
||||
return age >= -5 * 60 * 1000 && age <= VERIFICATION_CODE_MAX_AGE_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* The code for a list row, from the subject and preview the list already
|
||||
* has; older mail is skipped without looking.
|
||||
*/
|
||||
export function listVerificationCode(
|
||||
email: Pick<Email, 'subject' | 'preview' | 'receivedAt'>,
|
||||
now = Date.now(),
|
||||
): string | null {
|
||||
if (!isFreshForVerificationCode(email.receivedAt, now)) return null;
|
||||
return findVerificationCode(email.subject, email.preview);
|
||||
}
|
||||
|
||||
// A code sits near the top; a newsletter of megabytes need not be read whole.
|
||||
const MAX_BODY_HTML = 200_000;
|
||||
const MAX_BODY_TEXT = 50_000;
|
||||
|
||||
// Every code word above contains one of these (any case), so a body without
|
||||
// any of them has no code to find, and the HTML need not be turned into text.
|
||||
const CODE_WORD_HINT = /cod|cód|kod|kód|kood|код|κωδικ|קוד|رمز|کد|kennwort|passwort|pass(?:word|code)|コード|認証番号|確認番号|验证码|驗證碼|校验码|动态码|인증|코드/iu;
|
||||
const UPPERCASE_HINT = /PIN|OTP|TAN/;
|
||||
// Entities that stand for no letter, decoded so they can't hide a code word.
|
||||
const NAMED_ENTITIES: Record<string, string> = {
|
||||
amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', ensp: ' ', emsp: ' ', thinsp: ' ',
|
||||
shy: '', zwnj: '', zwj: '', lrm: '', rlm: '', copy: '', reg: '', trade: '', hellip: '', mdash: '',
|
||||
ndash: '', lsquo: '', rsquo: '', ldquo: '', rdquo: '', laquo: '', raquo: '', bull: '', middot: '',
|
||||
euro: '', pound: '', cent: '', yen: '', deg: '', times: '', sect: '', para: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* False when the HTML cannot hold a code word, so verificationCodeBodyText
|
||||
* can skip parsing it (the costly part of opening a big HTML mail). Errs on
|
||||
* the side of true: tags are dropped so a word split by markup joins up
|
||||
* again, numeric entities are decoded, invisible spacers removed, and any
|
||||
* named entity that might be a letter counts as a possible code word.
|
||||
*/
|
||||
export function htmlMayNameACode(html: string): boolean {
|
||||
let unknownEntity = false;
|
||||
const text = html
|
||||
// `[^<>]` so a `<` without a `>` after it is given up at the next `<`,
|
||||
// not after scanning to the end once per `<`.
|
||||
.replace(/<[^<>]*>/g, '')
|
||||
.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);/gi, (_m, ref: string) => {
|
||||
if (ref[0] === '#') {
|
||||
const cp = ref[1] === 'x' || ref[1] === 'X' ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
|
||||
return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '';
|
||||
}
|
||||
const known = NAMED_ENTITIES[ref.toLowerCase()];
|
||||
if (known === undefined) unknownEntity = true;
|
||||
return known ?? '';
|
||||
})
|
||||
.replace(/[\u00ad\u200b-\u200f\u2060\ufeff]|\u034f/g, '');
|
||||
return unknownEntity || CODE_WORD_HINT.test(text) || UPPERCASE_HINT.test(text);
|
||||
}
|
||||
|
||||
// HTML 4 entities that stand for a letter (Latin-1 and Greek): a code word
|
||||
// such as "código" has to read as one word.
|
||||
const LETTER_ENTITIES: Record<string, string> = {
|
||||
AElig: 'Æ', Aacute: 'Á', Acirc: 'Â', Agrave: 'À', Alpha: 'Α', Aring: 'Å', Atilde: 'Ã', Auml: 'Ä',
|
||||
Beta: 'Β', Ccedil: 'Ç', Chi: 'Χ', Delta: 'Δ', ETH: 'Ð', Eacute: 'É', Ecirc: 'Ê', Egrave: 'È',
|
||||
Epsilon: 'Ε', Eta: 'Η', Euml: 'Ë', Gamma: 'Γ', Iacute: 'Í', Icirc: 'Î', Igrave: 'Ì', Iota: 'Ι',
|
||||
Iuml: 'Ï', Kappa: 'Κ', Lambda: 'Λ', Mu: 'Μ', Ntilde: 'Ñ', Nu: 'Ν', OElig: 'Œ', Oacute: 'Ó',
|
||||
Ocirc: 'Ô', Ograve: 'Ò', Omega: 'Ω', Omicron: 'Ο', Oslash: 'Ø', Otilde: 'Õ', Ouml: 'Ö', Phi: 'Φ',
|
||||
Pi: 'Π', Psi: 'Ψ', Rho: 'Ρ', Scaron: 'Š', Sigma: 'Σ', THORN: 'Þ', Tau: 'Τ', Theta: 'Θ',
|
||||
Uacute: 'Ú', Ucirc: 'Û', Ugrave: 'Ù', Upsilon: 'Υ', Uuml: 'Ü', Xi: 'Ξ', Yacute: 'Ý', Yuml: 'Ÿ',
|
||||
Zeta: 'Ζ', aacute: 'á', acirc: 'â', aelig: 'æ', agrave: 'à', alefsym: 'ℵ', alpha: 'α', aring: 'å',
|
||||
atilde: 'ã', auml: 'ä', beta: 'β', ccedil: 'ç', chi: 'χ', circ: 'ˆ', delta: 'δ', eacute: 'é',
|
||||
ecirc: 'ê', egrave: 'è', epsilon: 'ε', eta: 'η', eth: 'ð', euml: 'ë', fnof: 'ƒ', gamma: 'γ',
|
||||
iacute: 'í', icirc: 'î', igrave: 'ì', image: 'ℑ', iota: 'ι', iuml: 'ï', kappa: 'κ', lambda: 'λ',
|
||||
micro: 'µ', mu: 'μ', ntilde: 'ñ', nu: 'ν', oacute: 'ó', ocirc: 'ô', oelig: 'œ', ograve: 'ò',
|
||||
omega: 'ω', omicron: 'ο', ordf: 'ª', ordm: 'º', oslash: 'ø', otilde: 'õ', ouml: 'ö', phi: 'φ',
|
||||
pi: 'π', piv: 'ϖ', psi: 'ψ', real: 'ℜ', rho: 'ρ', scaron: 'š', sigma: 'σ', sigmaf: 'ς',
|
||||
szlig: 'ß', tau: 'τ', theta: 'θ', thetasym: 'ϑ', thorn: 'þ', uacute: 'ú', ucirc: 'û', ugrave: 'ù',
|
||||
upsih: 'ϒ', upsilon: 'υ', uuml: 'ü', xi: 'ξ', yacute: 'ý', yuml: 'ÿ', zeta: 'ζ',
|
||||
};
|
||||
|
||||
function decodeEntities(text: string): string {
|
||||
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);/gi, (m, ref: string) => {
|
||||
if (ref[0] === '#') {
|
||||
const cp = ref[1] === 'x' || ref[1] === 'X' ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
|
||||
return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '';
|
||||
}
|
||||
return LETTER_ENTITIES[ref] ?? NAMED_ENTITIES[ref.toLowerCase()] ?? m;
|
||||
});
|
||||
}
|
||||
|
||||
// Elements whose boundaries end a line of text.
|
||||
const BLOCK_TAG = /<\/?(?:p|div|tr|li|ul|ol|h[1-6]|table|tbody|thead|blockquote|pre|section|article|header|footer|center|hr)\b[^<>]*>/gi;
|
||||
|
||||
/**
|
||||
* HTML to text for the code search. The webmail parses it with the DOM;
|
||||
* here every pattern stops at the next `<`, so a body of `<` without `>`
|
||||
* costs one pass, not one pass per `<` (the composer's htmlToPlainText would).
|
||||
*/
|
||||
function markupToText(html: string): string {
|
||||
return decodeEntities(
|
||||
html
|
||||
.replace(/<br\b[^<>]*>/gi, '\n')
|
||||
.replace(BLOCK_TAG, '\n')
|
||||
.replace(/<\/?t[dh]\b[^<>]*>/gi, ' ')
|
||||
.replace(/<[^<>]*>/g, ''),
|
||||
)
|
||||
.replace(/[^\S\n]+/g, ' ')
|
||||
.replace(/ ?\n\s*/g, '\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** The text of a loaded mail as the reader shows it, for finding its code. */
|
||||
export function verificationCodeBodyText(email: Pick<Email, 'htmlBody' | 'textBody' | 'bodyValues'>): string {
|
||||
const picked = pickEmailBody(email);
|
||||
const html = selectRenderableHtml(picked);
|
||||
if (html) {
|
||||
// htmlToPlainText walks the body, and a <style> in it is text there.
|
||||
// Cut first, then strip: an unclosed element runs to the end of what is
|
||||
// left, once, rather than to the end of the whole body once per opener.
|
||||
const markup = html
|
||||
.slice(0, MAX_BODY_HTML)
|
||||
.replace(/<(style|script|title)\b[^<>]*>[\s\S]*?(?:<\/\1\s*>|$)/gi, ' ');
|
||||
if (!htmlMayNameACode(markup)) return '';
|
||||
return markupToText(markup).slice(0, MAX_BODY_TEXT);
|
||||
}
|
||||
return (picked.text || '').slice(0, MAX_BODY_TEXT);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { OfflineBanner } from '../components/OfflineBanner';
|
||||
import {
|
||||
ListAttachmentChips, ListAttachmentOpener, useListRowAttachments,
|
||||
} from '../components/email/ListAttachmentChips';
|
||||
import { VerificationCodeChip, useListVerificationCode } from '../components/email/VerificationCodeChip';
|
||||
import type { LoadListAttachments } from '../lib/list-attachments';
|
||||
import { useNetworkStore } from '../stores/network-store';
|
||||
import {
|
||||
@@ -172,6 +173,7 @@ const EmailRow = React.memo(function EmailRow({
|
||||
const key = rowKeyOf(item);
|
||||
const handlePress = React.useCallback(() => onPress(key), [onPress, key]);
|
||||
const handleLongPress = React.useCallback(() => onLongPress(key), [onLongPress, key]);
|
||||
const verificationCode = useListVerificationCode(item);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -270,6 +272,7 @@ const EmailRow = React.memo(function EmailRow({
|
||||
{singleLine(item.preview)}
|
||||
</Text>
|
||||
)}
|
||||
{verificationCode && <VerificationCodeChip code={verificationCode} disabled={selectionMode} inList />}
|
||||
{onOpenAttachment && (
|
||||
<ListAttachmentChips
|
||||
email={item}
|
||||
|
||||
@@ -185,6 +185,9 @@ interface PersistedSettings {
|
||||
deleteAction: DeleteAction;
|
||||
permanentlyDeleteJunk: boolean;
|
||||
showPreview: boolean;
|
||||
// Offer the one-time code of a sign-in mail as a copy chip in the list
|
||||
// (for a day) and in the reader.
|
||||
showVerificationCodes: boolean;
|
||||
emailsPerPage: number;
|
||||
// Mail list sort order: oldest-first when true. Applies to every mailbox
|
||||
// (the JMAP Email/query sorts by receivedAt).
|
||||
@@ -370,6 +373,7 @@ const DEFAULT_PERSISTED: PersistedSettings = {
|
||||
deleteAction: 'trash',
|
||||
permanentlyDeleteJunk: false,
|
||||
showPreview: true,
|
||||
showVerificationCodes: true,
|
||||
emailsPerPage: 25,
|
||||
mailSortAscending: false,
|
||||
disableThreading: false,
|
||||
|
||||
Reference in New Issue
Block a user