From 727c1ab3e276f5b86b46f954e3ae28d3f767c81e Mon Sep 17 00:00:00 2001 From: Meylis Annagurbanov Date: Mon, 11 May 2026 17:25:32 +0500 Subject: [PATCH 1/5] fix(security): redact email PII from auth-flow logs (#4254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Email addresses were logged in plaintext at 6 sites across the auth flow (3 in PendingVerificationService, 2 in DivineAuthCubit, 1 in EmailVerificationCubit). Flagged in #3784's review (point 4) but kept out of the emergency-security scope of that PR — addressed here as the systematic fix. Approach: - New `redactEmailForLogs(String)` helper in lib/utils/sensitive_uri_for_logs.dart. Partial-redaction (`user@example.com` → `u***@example.com`) preserves the domain so ops can correlate failure patterns per provider without identifying individual accounts. Empty / malformed input returns the existing `redactedSensitiveLogPlaceholder`. - Broadened `sanitizeForCrashReport` in lib/observability/reportable_error.dart to also strip emails before forwarding to Crashlytics — defense-in-depth so any future call site that forgets the helper still gets sanitized when its error flows through `Reportable.toString()`. - All 6 call sites on origin/main wrapped with the helper. The 7th site (the legacy-nsec migration warning added by #3784) is intentionally NOT included here — whichever of #3784 / this PR merges second receives a small rebase to wrap that site too. Tests: - 9 new cases for `redactEmailForLogs` (standard, single-char, long local-part, subdomains, empty, no-`@`, empty local-part, no-TLD domain, whitespace). - 4 new cases for `sanitizeForCrashReport` covering email stripping, multiple emails, mixed npub/nsec/email, and domain preservation. Closes #4254. --- .../blocs/divine_auth/divine_auth_cubit.dart | 5 +- .../email_verification_cubit.dart | 3 +- .../lib/observability/reportable_error.dart | 31 ++++++--- .../pending_verification_service.dart | 8 ++- mobile/lib/utils/sensitive_uri_for_logs.dart | 31 ++++++++- .../observability/reportable_error_test.dart | 43 ++++++++++++ .../utils/sensitive_uri_for_logs_test.dart | 65 +++++++++++++++++++ 7 files changed, 170 insertions(+), 16 deletions(-) diff --git a/mobile/lib/blocs/divine_auth/divine_auth_cubit.dart b/mobile/lib/blocs/divine_auth/divine_auth_cubit.dart index 8b90e96558..57feefc557 100644 --- a/mobile/lib/blocs/divine_auth/divine_auth_cubit.dart +++ b/mobile/lib/blocs/divine_auth/divine_auth_cubit.dart @@ -9,6 +9,7 @@ import 'package:nostr_key_manager/nostr_key_manager.dart'; import 'package:openvine/services/auth_service.dart'; import 'package:openvine/services/pending_verification_service.dart'; import 'package:openvine/utils/invite_error_utils.dart'; +import 'package:openvine/utils/sensitive_uri_for_logs.dart'; import 'package:openvine/utils/validators.dart'; import 'package:unified_logger/unified_logger.dart'; @@ -272,7 +273,7 @@ class DivineAuthCubit extends Cubit { if (result.verificationRequired && result.deviceCode != null) { Log.info( - 'Email verification required for $email', + 'Email verification required for ${redactEmailForLogs(email)}', name: 'DivineAuthCubit', category: LogCategory.auth, ); @@ -385,7 +386,7 @@ class DivineAuthCubit extends Cubit { /// Send password reset email Future sendPasswordResetEmail(String email) async { Log.info( - 'Sending password reset email to $email', + 'Sending password reset email to ${redactEmailForLogs(email)}', name: 'DivineAuthCubit', category: LogCategory.auth, ); diff --git a/mobile/lib/blocs/email_verification/email_verification_cubit.dart b/mobile/lib/blocs/email_verification/email_verification_cubit.dart index f662af45da..05da0a72f9 100644 --- a/mobile/lib/blocs/email_verification/email_verification_cubit.dart +++ b/mobile/lib/blocs/email_verification/email_verification_cubit.dart @@ -10,6 +10,7 @@ import 'package:invite_api_client/invite_api_client.dart'; import 'package:keycast_flutter/keycast_flutter.dart'; import 'package:openvine/services/auth_service.dart'; import 'package:openvine/utils/invite_error_utils.dart'; +import 'package:openvine/utils/sensitive_uri_for_logs.dart'; import 'package:unified_logger/unified_logger.dart'; part 'email_verification_state.dart'; @@ -72,7 +73,7 @@ class EmailVerificationCubit extends Cubit { String? inviteCode, }) { Log.info( - 'startPolling called for $email ' + 'startPolling called for ${redactEmailForLogs(email)} ' '(cubit=$hashCode, authSvc=${_authService.hashCode}, ' 'hasExistingTimer=${_pollTimer != null})', name: 'EmailVerificationCubit', diff --git a/mobile/lib/observability/reportable_error.dart b/mobile/lib/observability/reportable_error.dart index 1deba6f0b7..e57e6b67d0 100644 --- a/mobile/lib/observability/reportable_error.dart +++ b/mobile/lib/observability/reportable_error.dart @@ -56,17 +56,32 @@ final class Reportable implements ReportableError { final RegExp _npubPattern = RegExp('npub1[a-z0-9]+'); final RegExp _nsecPattern = RegExp('nsec1[a-z0-9]+'); +// Conservative email matcher: local-part of common chars then `@`, host with +// at least one `.`. Intentionally narrower than RFC 5322 — false negatives on +// exotic addresses are preferable to false positives on non-email strings. +final RegExp _emailPattern = RegExp( + r'[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}', +); -/// Strips Nostr `npub1…` and `nsec1…` identifiers from [input] before it is -/// forwarded to a third-party crash reporter. +/// Strips Nostr `npub1…` / `nsec1…` identifiers and email addresses from +/// [input] before it is forwarded to a third-party crash reporter. /// -/// Scope is intentionally narrow: only `npub` (public-key bech32) and `nsec` -/// (private-key bech32). Other Nostr-format references (`note1`, `nevent1`, -/// `nprofile1`) encode event/profile pointers, not secrets, and removing them -/// removes triage value. Call sites that need to redact those should do so -/// explicitly before constructing the error message. +/// Scope is intentionally narrow: +/// - Nostr: only `npub` (public-key bech32) and `nsec` (private-key bech32). +/// Other Nostr-format references (`note1`, `nevent1`, `nprofile1`) encode +/// event/profile pointers, not secrets, and removing them removes triage +/// value. Call sites that need to redact those should do so explicitly +/// before constructing the error message. +/// - Email: any RFC-5321-ish local@host.tld pattern. Replaced with a +/// first-char-preserving partial mask so domain-level correlation survives. String sanitizeForCrashReport(String input) { return input .replaceAll(_npubPattern, 'npub1') - .replaceAll(_nsecPattern, 'nsec1'); + .replaceAll(_nsecPattern, 'nsec1') + .replaceAllMapped(_emailPattern, (match) { + final email = match.group(0)!; + final atIndex = email.indexOf('@'); + // _emailPattern guarantees atIndex > 0 and a domain follows. + return '${email.substring(0, 1)}***@${email.substring(atIndex + 1)}'; + }); } diff --git a/mobile/lib/services/pending_verification_service.dart b/mobile/lib/services/pending_verification_service.dart index 4e046dd51b..2180d7891b 100644 --- a/mobile/lib/services/pending_verification_service.dart +++ b/mobile/lib/services/pending_verification_service.dart @@ -2,6 +2,7 @@ // ABOUTME: Enables auto-login when app is cold-started via email verification deep link import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:openvine/utils/sensitive_uri_for_logs.dart'; import 'package:unified_logger/unified_logger.dart'; /// Data class representing pending email verification credentials @@ -65,7 +66,7 @@ class PendingVerificationService { _storage.write(key: _keyInviteCode, value: inviteCode), ]); Log.info( - 'Saved pending verification for $email', + 'Saved pending verification for ${redactEmailForLogs(email)}', name: 'PendingVerificationService', category: LogCategory.auth, ); @@ -121,7 +122,8 @@ class PendingVerificationService { // Check expiration if (pending.isExpired) { Log.info( - 'Pending verification for $email has expired, clearing', + 'Pending verification for ${redactEmailForLogs(email)} has expired, ' + 'clearing', name: 'PendingVerificationService', category: LogCategory.auth, ); @@ -130,7 +132,7 @@ class PendingVerificationService { } Log.info( - 'Loaded pending verification for $email', + 'Loaded pending verification for ${redactEmailForLogs(email)}', name: 'PendingVerificationService', category: LogCategory.auth, ); diff --git a/mobile/lib/utils/sensitive_uri_for_logs.dart b/mobile/lib/utils/sensitive_uri_for_logs.dart index ad6c765a64..1657de91e5 100644 --- a/mobile/lib/utils/sensitive_uri_for_logs.dart +++ b/mobile/lib/utils/sensitive_uri_for_logs.dart @@ -1,5 +1,7 @@ -// ABOUTME: Redacts secrets from URIs before writing them to diagnostic logs. -// ABOUTME: Preserves schemes, hosts, paths, and Nostr path segments unchanged except /invite/code. +// ABOUTME: Redacts secrets and PII from URIs and free-form text before +// ABOUTME: writing them to diagnostic logs (URIs, email addresses, Nostr keys +// ABOUTME: are handled here; Nostr key stripping for crash reports lives in +// ABOUTME: lib/observability/reportable_error.dart). /// Placeholder for secrets in free-form log text or HTTP headers. const redactedSensitiveLogPlaceholder = '[REDACTED]'; @@ -58,3 +60,28 @@ String redactUriStringForLogs(String uriString) { } return out; } + +/// Returns an email address safe for diagnostic logs. +/// +/// - `user@example.com` → `u***@example.com` +/// - `a@b.co` → `a***@b.co` (single-char local-part still gets +/// a fixed-width mask so the original +/// length is not leaked) +/// - empty input or input missing `@` or with no `.` in the domain → +/// [redactedSensitiveLogPlaceholder] +/// +/// The domain is preserved verbatim so ops can correlate failure patterns +/// across the same provider (e.g. "all gmail.com users are timing out") +/// without identifying individual accounts. Local-part collapses to a +/// fixed 4-character mask (`x***`) regardless of length. +String redactEmailForLogs(String email) { + if (email.isEmpty) return redactedSensitiveLogPlaceholder; + final atIndex = email.indexOf('@'); + // No `@`, or `@` at position 0 (empty local-part) → not a usable email. + if (atIndex <= 0) return redactedSensitiveLogPlaceholder; + final domain = email.substring(atIndex + 1); + // Domain must contain at least one `.` to be a routable host. + if (!domain.contains('.')) return redactedSensitiveLogPlaceholder; + final firstChar = email.substring(0, 1); + return '$firstChar***@$domain'; +} diff --git a/mobile/test/observability/reportable_error_test.dart b/mobile/test/observability/reportable_error_test.dart index da27742b38..0ce8d57c58 100644 --- a/mobile/test/observability/reportable_error_test.dart +++ b/mobile/test/observability/reportable_error_test.dart @@ -111,5 +111,48 @@ void main() { expect(sanitizeForCrashReport(input), equals(input)); }); + + // Issue #4254 — emails are PII and must not flow to Crashlytics. + test('replaces an email with the first-char + domain mask', () { + const input = 'verification failed for user@example.com after 3 retries'; + + expect( + sanitizeForCrashReport(input), + 'verification failed for u***@example.com after 3 retries', + ); + }); + + test('replaces multiple emails in the same string', () { + const input = 'forwarded alice@a.com to bob@b.io'; + final out = sanitizeForCrashReport(input); + + expect(out, contains('a***@a.com')); + expect(out, contains('b***@b.io')); + expect(out, isNot(contains('alice'))); + expect(out, isNot(contains('bob'))); + }); + + test('strips email PII alongside npub / nsec in one pass', () { + const input = + 'user@example.com leaked nsec1qwertyuiopasdfghjklzxcvbnm0123456789ab ' + 'tied to npub1abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnop'; + final out = sanitizeForCrashReport(input); + + expect(out, contains('u***@example.com')); + expect(out, contains('nsec1')); + expect(out, contains('npub1')); + expect(out, isNot(contains('user@example.com'))); + }); + + test('preserves the domain so ops can correlate failure patterns', () { + // The whole point of partial (not opaque) redaction: ops can spot + // "all gmail.com users are failing" without identifying anyone. + const input = 'auth failed for alice@gmail.com'; + + expect( + sanitizeForCrashReport(input), + contains('@gmail.com'), + ); + }); }); } diff --git a/mobile/test/unit/utils/sensitive_uri_for_logs_test.dart b/mobile/test/unit/utils/sensitive_uri_for_logs_test.dart index f06a1c3504..4c9645bf04 100644 --- a/mobile/test/unit/utils/sensitive_uri_for_logs_test.dart +++ b/mobile/test/unit/utils/sensitive_uri_for_logs_test.dart @@ -122,4 +122,69 @@ void main() { expect(out, contains('code=$redactedUriComponentForLogs')); }); }); + + /// Issue #4254 — redact email PII from auth-flow logs. + group('redactEmailForLogs', () { + test('partial-redacts a standard email, preserves domain', () { + expect( + redactEmailForLogs('user@example.com'), + equals('u***@example.com'), + ); + }); + + test('partial-redacts a single-character local-part', () { + // Even one-char local-parts get the fixed `x***` mask — the original + // length must not be leaked. + expect(redactEmailForLogs('a@b.co'), equals('a***@b.co')); + }); + + test('preserves subdomains in the domain part', () { + expect( + redactEmailForLogs('alice@mail.corp.example.com'), + equals('a***@mail.corp.example.com'), + ); + }); + + test('hides the full local-part even when it is long', () { + final out = redactEmailForLogs('first.last+tag@example.com'); + expect(out, equals('f***@example.com')); + expect(out, isNot(contains('first'))); + expect(out, isNot(contains('last'))); + expect(out, isNot(contains('tag'))); + }); + + test('returns the opaque placeholder for empty input', () { + expect(redactEmailForLogs(''), equals(redactedSensitiveLogPlaceholder)); + }); + + test('returns the opaque placeholder for input without `@`', () { + expect( + redactEmailForLogs('not-an-email'), + equals(redactedSensitiveLogPlaceholder), + ); + }); + + test('returns the opaque placeholder for empty local-part', () { + expect( + redactEmailForLogs('@example.com'), + equals(redactedSensitiveLogPlaceholder), + ); + }); + + test('returns the opaque placeholder for a domain without a dot', () { + // `a@b` (no TLD) is not a routable host — fail closed. + expect( + redactEmailForLogs('a@b'), + equals(redactedSensitiveLogPlaceholder), + ); + }); + + test('returns the opaque placeholder for whitespace-only input', () { + // No `@` → treated like any other malformed input. + expect( + redactEmailForLogs(' '), + equals(redactedSensitiveLogPlaceholder), + ); + }); + }); } From 87d53faa31fe8d08a44a3dd014a2d0d1617572ba Mon Sep 17 00:00:00 2001 From: Liz Sweigart <127434495+NotThatKindOfDrLiz@users.noreply.github.com> Date: Mon, 11 May 2026 12:25:21 -0500 Subject: [PATCH 2/5] fix(security): redact persisted verification email log --- .../auth/email_verification_screen.dart | 4 +- .../auth/email_verification_screen_test.dart | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/mobile/lib/screens/auth/email_verification_screen.dart b/mobile/lib/screens/auth/email_verification_screen.dart index 0be00341ae..e5d398490a 100644 --- a/mobile/lib/screens/auth/email_verification_screen.dart +++ b/mobile/lib/screens/auth/email_verification_screen.dart @@ -24,6 +24,7 @@ import 'package:openvine/providers/route_feed_providers.dart'; import 'package:openvine/screens/auth/welcome_screen.dart'; import 'package:openvine/screens/explore_screen.dart'; import 'package:openvine/services/auth_service.dart'; +import 'package:openvine/utils/sensitive_uri_for_logs.dart'; import 'package:unified_logger/unified_logger.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -150,7 +151,8 @@ class _EmailVerificationScreenState if (pending != null) { Log.info( - 'Found persisted verification data for ${pending.email}, ' + 'Found persisted verification data for ' + '${redactEmailForLogs(pending.email)}, ' 'attempting auto-login flow', name: 'EmailVerificationScreen', category: LogCategory.auth, diff --git a/mobile/test/screens/auth/email_verification_screen_test.dart b/mobile/test/screens/auth/email_verification_screen_test.dart index 05b282a92a..694990f741 100644 --- a/mobile/test/screens/auth/email_verification_screen_test.dart +++ b/mobile/test/screens/auth/email_verification_screen_test.dart @@ -21,6 +21,7 @@ import 'package:openvine/providers/route_feed_providers.dart'; import 'package:openvine/screens/auth/email_verification_screen.dart'; import 'package:openvine/services/auth_service.dart'; import 'package:openvine/services/pending_verification_service.dart'; +import 'package:unified_logger/unified_logger.dart'; import '../../helpers/test_provider_overrides.dart'; @@ -390,6 +391,55 @@ void main() { verify(() => mockCubit.stopPolling()).called(greaterThan(0)); }); + testWidgets('redacts persisted pending email in auto-login logs', ( + tester, + ) async { + await LogCaptureService().clearAllLogs(); + when(() => mockPendingVerification.load()).thenAnswer( + (_) async => PendingVerification( + deviceCode: 'persisted-device-code', + verifier: 'persisted-verifier', + email: 'user@example.com', + createdAt: DateTime(2026), + ), + ); + when( + () => mockOAuth.verifyEmail(token: any(named: 'token')), + ).thenAnswer((_) async => VerifyEmailResult(success: true)); + when( + () => mockCubit.startPolling( + deviceCode: any(named: 'deviceCode'), + verifier: any(named: 'verifier'), + email: any(named: 'email'), + inviteCode: any(named: 'inviteCode'), + ), + ).thenReturn(null); + + await tester.pumpWidget(createTestWidget(token: 'persisted-token')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + + final logMessage = LogCaptureService() + .getRecentLogs() + .map((entry) => entry.message) + .lastWhere( + (message) => + message.startsWith('Found persisted verification data for '), + ); + + expect(logMessage, contains('u***@example.com')); + expect(logMessage, isNot(contains('user@example.com'))); + verify(() => mockOAuth.verifyEmail(token: 'persisted-token')).called(1); + verify( + () => mockCubit.startPolling( + deviceCode: 'persisted-device-code', + verifier: 'persisted-verifier', + email: 'user@example.com', + inviteCode: null, + ), + ).called(1); + }); + testWidgets( 're-verifies when token changes while already in token mode', (tester) async { From c6f3703dd85b711f8a376eea579d0b4ca641521c Mon Sep 17 00:00:00 2001 From: Liz Sweigart <127434495+NotThatKindOfDrLiz@users.noreply.github.com> Date: Mon, 11 May 2026 12:31:42 -0500 Subject: [PATCH 3/5] fix(test): drop redundant mock argument --- mobile/test/screens/auth/email_verification_screen_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/test/screens/auth/email_verification_screen_test.dart b/mobile/test/screens/auth/email_verification_screen_test.dart index 694990f741..49b02f9843 100644 --- a/mobile/test/screens/auth/email_verification_screen_test.dart +++ b/mobile/test/screens/auth/email_verification_screen_test.dart @@ -435,7 +435,6 @@ void main() { deviceCode: 'persisted-device-code', verifier: 'persisted-verifier', email: 'user@example.com', - inviteCode: null, ), ).called(1); }); From cacc67541131d37c871003267e7d64033c0e5c8b Mon Sep 17 00:00:00 2001 From: Liz Sweigart <127434495+NotThatKindOfDrLiz@users.noreply.github.com> Date: Mon, 11 May 2026 13:10:45 -0500 Subject: [PATCH 4/5] fix(security): share email redaction between logs and crash reports --- mobile/lib/observability/reportable_error.dart | 17 +++++++++-------- .../observability/reportable_error_test.dart | 10 ++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/mobile/lib/observability/reportable_error.dart b/mobile/lib/observability/reportable_error.dart index e57e6b67d0..88da27a046 100644 --- a/mobile/lib/observability/reportable_error.dart +++ b/mobile/lib/observability/reportable_error.dart @@ -1,6 +1,8 @@ // ABOUTME: Marker interface + wrapper + PII sanitizer used to gate Bloc errors // ABOUTME: forwarded to Crashlytics. See .claude/rules/error_handling.md. +import 'package:openvine/utils/sensitive_uri_for_logs.dart'; + /// Marker interface signalling that an error is worth reporting to Crashlytics. /// /// Errors that flow through `addError(error, st)` only reach the crash reporter @@ -72,16 +74,15 @@ final RegExp _emailPattern = RegExp( /// event/profile pointers, not secrets, and removing them removes triage /// value. Call sites that need to redact those should do so explicitly /// before constructing the error message. -/// - Email: any RFC-5321-ish local@host.tld pattern. Replaced with a -/// first-char-preserving partial mask so domain-level correlation survives. +/// - Email: any RFC-5321-ish local@host.tld pattern. Replacement delegates to +/// [redactEmailForLogs] so log redaction and crash-report redaction stay in +/// lockstep. String sanitizeForCrashReport(String input) { return input .replaceAll(_npubPattern, 'npub1') .replaceAll(_nsecPattern, 'nsec1') - .replaceAllMapped(_emailPattern, (match) { - final email = match.group(0)!; - final atIndex = email.indexOf('@'); - // _emailPattern guarantees atIndex > 0 and a domain follows. - return '${email.substring(0, 1)}***@${email.substring(atIndex + 1)}'; - }); + .replaceAllMapped( + _emailPattern, + (match) => redactEmailForLogs(match.group(0)!), + ); } diff --git a/mobile/test/observability/reportable_error_test.dart b/mobile/test/observability/reportable_error_test.dart index 0ce8d57c58..71c93310de 100644 --- a/mobile/test/observability/reportable_error_test.dart +++ b/mobile/test/observability/reportable_error_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:openvine/observability/reportable_error.dart'; +import 'package:openvine/utils/sensitive_uri_for_logs.dart'; void main() { group(Reportable, () { @@ -154,5 +155,14 @@ void main() { contains('@gmail.com'), ); }); + + test('uses the shared email redactor so masking stays aligned', () { + const email = 'first.last+tag@example.com'; + + expect( + sanitizeForCrashReport('auth failed for $email'), + 'auth failed for ${redactEmailForLogs(email)}', + ); + }); }); } From fb4b6e56b6047b991fb0cf51c0180c01beeb833c Mon Sep 17 00:00:00 2001 From: Liz Sweigart <127434495+NotThatKindOfDrLiz@users.noreply.github.com> Date: Mon, 11 May 2026 13:23:02 -0500 Subject: [PATCH 5/5] docs(security): clarify shared email redaction path --- mobile/lib/observability/reportable_error.dart | 1 + mobile/test/observability/reportable_error_test.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/mobile/lib/observability/reportable_error.dart b/mobile/lib/observability/reportable_error.dart index 88da27a046..07d821bb82 100644 --- a/mobile/lib/observability/reportable_error.dart +++ b/mobile/lib/observability/reportable_error.dart @@ -83,6 +83,7 @@ String sanitizeForCrashReport(String input) { .replaceAll(_nsecPattern, 'nsec1') .replaceAllMapped( _emailPattern, + // Reuse the log helper here so both redaction surfaces stay aligned. (match) => redactEmailForLogs(match.group(0)!), ); } diff --git a/mobile/test/observability/reportable_error_test.dart b/mobile/test/observability/reportable_error_test.dart index 71c93310de..a28e8c1ab5 100644 --- a/mobile/test/observability/reportable_error_test.dart +++ b/mobile/test/observability/reportable_error_test.dart @@ -160,6 +160,7 @@ void main() { const email = 'first.last+tag@example.com'; expect( + // This locks the crash-report fallback to the same mask shape as logs. sanitizeForCrashReport('auth failed for $email'), 'auth failed for ${redactEmailForLogs(email)}', );