Skip to content

Email sender/branding based on user's registration wallet instead of request origin #3595

@TaprootFreak

Description

@TaprootFreak

Problem

All user-facing emails (login, KYC, transaction confirmations, etc.) are sent with the sender identity and branding of the wallet the user originally registered with (userData.wallet / user.wallet), instead of the wallet/app context from which the current action originates.

This means: A customer who registered via the RealUnit app but later logs in on app.dfx.swiss receives their login email from "RealUnit" (via Infomaniak SMTP) instead of from "DFX.swiss" (via DFX SMTP). The reverse is also true: a DFX customer performing actions in the RealUnit app gets DFX-branded emails.

Expected Behavior

  • User logs in on app.dfx.swiss → email from DFX.swiss
  • User logs in on RealUnit app → email from RealUnit
  • User performs a buy/sell on DFX → transaction email from DFX.swiss
  • User performs a buy/sell on RealUnit → transaction email from RealUnit

The email sender should be determined by the context of the action (which app/platform initiated it), not by which wallet the user account is associated with.

Actual Behavior

The email sender is always determined by userData.wallet.name or entity.user.wallet.name, which is a static property set at account creation time. A user who registered via RealUnit will always receive RealUnit-branded emails, even when interacting with app.dfx.swiss.


Root Cause Analysis

Mail Configuration Chain

  1. Config (src/config/config.ts:629-640): A per-wallet mail config exists with dedicated SMTP credentials:

    wallet: {
      ...(process.env.REALUNIT_MAIL_USER && {
        RealUnit: {
          host: 'mail.infomaniak.com',
          port: 587,
          fromAddress: process.env.REALUNIT_MAIL_USER,
          displayName: 'RealUnit',
          template: 'user-v2',
        },
      }),
    }
  2. Mail base class (src/subdomains/supporting/notification/entities/mail/base/mail.ts:47-54): The wallet name is used to look up the mail config and determine sender identity:

    const walletMailConfig = params.walletName ? Config.mail.wallet[params.walletName] : undefined;
    this.#from = {
      name: params.displayName ?? walletMailConfig?.displayName ?? 'DFX.swiss',
      address: params.from ?? walletMailConfig?.fromAddress ?? Config.mail.contact.noReplyMail,
    };
  3. Mail transport (src/subdomains/supporting/notification/services/mail.service.ts:67-88): A separate SMTP transport is created per wallet config, so RealUnit mails go through a completely different mail server.

  4. UserMailV2 (src/subdomains/supporting/notification/entities/mail/user-mail-v2.ts:38-43): The wallet from the send call determines config lookup:

    const walletMailConfig = wallet?.name ? Config.mail.wallet[wallet.name] : undefined;

Where the Wrong Wallet Gets Passed

The wallet is passed in every notificationService.sendMail() call. In all cases, it's derived from a static entity relationship rather than the request context:

  • wallet: userData.wallet — the wallet stored on the user's account (set at registration)
  • wallet: entity.wallet / wallet: entity.user.wallet — derived from the User entity, which points back to the registration wallet

All Affected Locations (30 mail send calls)

Auth & Verification (2 files, 2 calls)

File Line Context Wallet Source
src/subdomains/generic/user/models/auth/auth.service.ts 300 LOGIN userData.wallet
src/subdomains/generic/kyc/services/tfa.service.ts 179 VERIFICATION_MAIL / EMAIL_VERIFICATION userData.wallet

KYC Notifications (1 file, 5 calls)

File Line Context Wallet Source
src/subdomains/generic/kyc/services/kyc-notification.service.ts 65 KYC_REMINDER userData.wallet
97 KYC_FAILED userData.wallet
139 KYC_MISSING_DATA userData.wallet
176 KYC_CHANGED userData.wallet
206 KYC_PAYMENT_DATA userData.wallet

Buy Crypto Notifications (1 file, 5 calls)

File Line Context Wallet Source
src/subdomains/core/buy-crypto/process/services/buy-crypto-notification.service.ts 95 BUY_CRYPTO_COMPLETED entity.walletentity.user.wallet
138 BUY_CRYPTO_PROCESSING entity.wallet
190 BUY_CRYPTO_PENDING entity.wallet
278 BUY_CRYPTO_RETURN entity.wallet
351 BUY_CRYPTO_CHARGEBACK_UNCONFIRMED entity.wallet

Buy Fiat (Sell) Notifications (1 file, 5 calls)

File Line Context Wallet Source
src/subdomains/core/sell-crypto/process/services/buy-fiat-notification.service.ts 54 BUY_FIAT_COMPLETED entity.walletentity.user.wallet
91 BUY_FIAT_PROCESSING entity.wallet
140 BUY_FIAT_PENDING entity.wallet
218 BUY_FIAT_RETURN entity.wallet
293 BUY_FIAT_CHARGEBACK_UNCONFIRMED entity.wallet

Transaction Notifications (1 file, 2 calls)

File Line Context Wallet Source
src/subdomains/supporting/payment/services/transaction-notification.service.ts 72 dynamic entity.user.wallet
136 UNASSIGNED_TX userData.wallet

User Data Notifications (1 file, 5 calls)

File Line Context Wallet Source
src/subdomains/generic/user/models/user-data/user-data-notification.service.ts 32 ACCOUNT_DEACTIVATION userData.wallet
65 ADDED_ADDRESS master.wallet
104 CHANGED_MAIL master.wallet
129 CHANGED_MAIL slave.wallet
177 BLACK_SQUAD entity.wallet

Other Notifications (5 files, 6 calls)

File Line Context Wallet Source
account-merge.service.ts 64 ACCOUNT_MERGE_REQUEST receiver.wallet
payin-notification.service.ts 48 CRYPTO_INPUT_RETURN entity.transaction.user.wallet
bank-tx-return-notification.service.ts 50 BANK_TX_RETURN entity.wallet
support-issue-notification.service.ts 19 SUPPORT_MESSAGE entity.userData.wallet
limit-request-notification.service.ts 46 LIMIT_REQUEST entity.userData.wallet
ref-reward-notification.service.ts 47 REF_REWARD entity.user.wallet
recommendation.service.ts 356 RECOMMENDATION_MAIL entity.recommended.wallet
recommendation.service.ts 401 RECOMMENDATION_CONFIRMATION entity.recommender.wallet

Complexity of the Fix

This is not a trivial fix because there are two categories of mail sends:

Category 1: Synchronous / Request-Context Available

These are triggered directly by a user action in an HTTP request. The request context (which app/wallet initiated it) is available:

  • Login mail (auth.service.ts): AuthMailDto.wallet already contains the requesting app's wallet name — but it's only used for account creation, not for the mail's wallet parameter
  • 2FA verification (tfa.service.ts): Called from controller, request context available
  • Email verification (user-data.service.ts): Called from controller

Fix approach: Pass the request-origin wallet to the mail send call instead of userData.wallet.

Category 2: Asynchronous / No Request Context

These are triggered by background jobs (cron), where no HTTP request context exists:

  • Transaction notifications (buy-crypto, buy-fiat, bank-tx-return, crypto-input-return)
  • KYC notifications (reminders, status changes)
  • Support issue notifications
  • Referral reward notifications

Fix approach: The origin wallet must be stored on the entity at creation time (e.g., which app/wallet the buy/sell was initiated from), so that async jobs can use it later. Alternatively, a new field like originWallet or mailWallet could be added to Transaction/BuyCrypto/BuyFiat entities.


Entity Relationships (for context)

Wallet (name: "RealUnit" | "DFX" | ...)
  └── User (has wallet — set at signup, one address = one user)
       └── UserData (has wallet — set at signup, shared across addresses)
            └── Transaction (has user → user.wallet)
                 └── BuyCrypto / BuyFiat (entity.user.wallet)
  • Wallet entity: src/subdomains/generic/user/models/wallet/wallet.entity.ts
  • User.wallet: ManyToOne → Wallet (line 40-41 in user.entity.ts)
  • UserData.wallet: ManyToOne → Wallet (line 368-369 in user-data.entity.ts)
  • BuyCrypto.wallet / BuyFiat.wallet: Computed getter → this.user.wallet

Suggested Approach

  1. For synchronous mails (login, 2FA, email verification): Use the wallet from the request context (e.g., AuthMailDto.wallet, origin header, or JWT) instead of userData.wallet
  2. For transaction-related async mails: Store an originWallet on the Transaction entity when the transaction is created (from the request context at that point), and use it for later notification emails
  3. For system-triggered mails (KYC reminders, account deactivation, etc.): Define a policy — likely default to userData.wallet since these aren't app-specific, or use the wallet of the most recent user interaction
  4. Consider a fallback chain: request-origin wallet → transaction origin wallet → userData.wallet → DFX default

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions