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
-
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',
},
}),
}
-
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,
};
-
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.
-
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.wallet → entity.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.wallet → entity.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
- 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
- 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
- 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
- Consider a fallback chain: request-origin wallet → transaction origin wallet → userData.wallet → DFX default
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.swissreceives 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
app.dfx.swiss→ email from DFX.swissThe 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.nameorentity.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 withapp.dfx.swiss.Root Cause Analysis
Mail Configuration Chain
Config (
src/config/config.ts:629-640): A per-wallet mail config exists with dedicated SMTP credentials: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: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.UserMailV2 (
src/subdomains/supporting/notification/entities/mail/user-mail-v2.ts:38-43): The wallet from the send call determines config lookup: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 walletAll Affected Locations (30 mail send calls)
Auth & Verification (2 files, 2 calls)
src/subdomains/generic/user/models/auth/auth.service.tsLOGINuserData.walletsrc/subdomains/generic/kyc/services/tfa.service.tsVERIFICATION_MAIL/EMAIL_VERIFICATIONuserData.walletKYC Notifications (1 file, 5 calls)
src/subdomains/generic/kyc/services/kyc-notification.service.tsKYC_REMINDERuserData.walletKYC_FAILEDuserData.walletKYC_MISSING_DATAuserData.walletKYC_CHANGEDuserData.walletKYC_PAYMENT_DATAuserData.walletBuy Crypto Notifications (1 file, 5 calls)
src/subdomains/core/buy-crypto/process/services/buy-crypto-notification.service.tsBUY_CRYPTO_COMPLETEDentity.wallet→entity.user.walletBUY_CRYPTO_PROCESSINGentity.walletBUY_CRYPTO_PENDINGentity.walletBUY_CRYPTO_RETURNentity.walletBUY_CRYPTO_CHARGEBACK_UNCONFIRMEDentity.walletBuy Fiat (Sell) Notifications (1 file, 5 calls)
src/subdomains/core/sell-crypto/process/services/buy-fiat-notification.service.tsBUY_FIAT_COMPLETEDentity.wallet→entity.user.walletBUY_FIAT_PROCESSINGentity.walletBUY_FIAT_PENDINGentity.walletBUY_FIAT_RETURNentity.walletBUY_FIAT_CHARGEBACK_UNCONFIRMEDentity.walletTransaction Notifications (1 file, 2 calls)
src/subdomains/supporting/payment/services/transaction-notification.service.tsentity.user.walletUNASSIGNED_TXuserData.walletUser Data Notifications (1 file, 5 calls)
src/subdomains/generic/user/models/user-data/user-data-notification.service.tsACCOUNT_DEACTIVATIONuserData.walletADDED_ADDRESSmaster.walletCHANGED_MAILmaster.walletCHANGED_MAILslave.walletBLACK_SQUADentity.walletOther Notifications (5 files, 6 calls)
account-merge.service.tsACCOUNT_MERGE_REQUESTreceiver.walletpayin-notification.service.tsCRYPTO_INPUT_RETURNentity.transaction.user.walletbank-tx-return-notification.service.tsBANK_TX_RETURNentity.walletsupport-issue-notification.service.tsSUPPORT_MESSAGEentity.userData.walletlimit-request-notification.service.tsLIMIT_REQUESTentity.userData.walletref-reward-notification.service.tsREF_REWARDentity.user.walletrecommendation.service.tsRECOMMENDATION_MAILentity.recommended.walletrecommendation.service.tsRECOMMENDATION_CONFIRMATIONentity.recommender.walletComplexity 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:
auth.service.ts):AuthMailDto.walletalready contains the requesting app's wallet name — but it's only used for account creation, not for the mail'swalletparametertfa.service.ts): Called from controller, request context availableuser-data.service.ts): Called from controllerFix 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:
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
originWalletormailWalletcould be added to Transaction/BuyCrypto/BuyFiat entities.Entity Relationships (for context)
Walletentity:src/subdomains/generic/user/models/wallet/wallet.entity.tsUser.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.walletSuggested Approach
AuthMailDto.wallet, origin header, or JWT) instead ofuserData.walletoriginWalleton the Transaction entity when the transaction is created (from the request context at that point), and use it for later notification emailsuserData.walletsince these aren't app-specific, or use the wallet of the most recent user interaction