/** * Shared metadata + mail sending for the merchant self-service e-mail fields * (/users/profile → E-Mails & Billing). * * EMAIL_FIELDS is the single source of truth for WHICH addresses a merchant * may manage, where each one lives (own ad_user row vs the company's * c_bpartner row) and what it is used for. The bpartner write keys mirror * MERCHANT_PARTNER_FIELDS in merchantPartner.ts (lowercase-first, per the * iDempiere REST write-casing rule). * * Field usage (why the labels/descriptions say what they say): * - eMail (C_BPartner.EMail): general company contact address — ticket * "your action required" notifications go here * (requests/[id]/action-notification.post.ts) and it is the fallback * whenever a more specific address below is empty. * - eMailInvoice (C_BPartner.EMail_Invoice): invoices are sent here. * - returnEmail (C_BPartner.return_email): customer-return notifications * (mobile/send-return-notification.post.ts) + CC on ticket action mails. * - freightServiceSenderMail (C_BPartner.freight_service_sender_mail): * passed to the freight service (DHL/DPD) as the sender contact e-mail * when shipping labels are created (commission/parcels/**). * - userEmail (AD_User.EMail): the user's own account address. */ import nodemailer from 'nodemailer' export const APP_BASE_URL = 'https://app.logship.de' // Same plausibility check on client and server. Deliberately simple: one @, // no whitespace, a dot in the domain. iDempiere itself stores plain strings. export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ export const isValidEmailSyntax = (value: string) => EMAIL_REGEX.test(value) && value.length <= 254 export interface EmailFieldDef { kind: 'bpartner' | 'aduser' writeKey: string // lowercase-first iDempiere REST write key required: boolean // required fields cannot be cleared to '' labelDe: string labelEn: string } export const EMAIL_FIELDS: Record = { userEmail: { kind: 'aduser', writeKey: 'eMail', required: true, labelDe: 'Eigene Konto-E-Mail', labelEn: 'Your account e-mail' }, eMail: { kind: 'bpartner', writeKey: 'eMail', required: true, labelDe: 'Allgemeine Kontakt-E-Mail', labelEn: 'General contact e-mail' }, eMailInvoice: { kind: 'bpartner', writeKey: 'eMail_Invoice', required: false, labelDe: 'Rechnungs-E-Mail', labelEn: 'Invoice e-mail' }, returnEmail: { kind: 'bpartner', writeKey: 'return_email', required: false, labelDe: 'Retouren-E-Mail', labelEn: 'Return e-mail' }, freightServiceSenderMail: { kind: 'bpartner', writeKey: 'freight_service_sender_mail', required: false, labelDe: 'Versanddienstleister Absender-E-Mail', labelEn: 'Freight service sender e-mail' } } const escapeHtml = (s: string) => String(s || '') .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, ''') const buildVerificationHtml = ( fieldLabel: string, oldEmail: string, newEmail: string, link: string, isGerman: boolean ) => { const title = isGerman ? 'Neue E-Mail-Adresse bestätigen' : 'Confirm your new e-mail address' const intro = isGerman ? 'Für Ihr LogShip-Konto wurde eine Änderung der folgenden E-Mail-Adresse angefordert. Die Änderung wird erst wirksam, wenn Sie sie über den Button unten bestätigen.' : 'A change of the following e-mail address was requested for your LogShip account. The change only takes effect once you confirm it via the button below.' const fieldLbl = isGerman ? 'Adresse' : 'Address' const oldLbl = isGerman ? 'Bisher' : 'Current' const newLbl = isGerman ? 'Neu' : 'New' const cta = isGerman ? 'E-Mail-Adresse bestätigen' : 'Confirm e-mail address' const expiry = isGerman ? 'Der Link ist 48 Stunden gültig. Bis zur Bestätigung bleibt die bisherige Adresse aktiv.' : 'The link is valid for 48 hours. Until confirmed, the current address stays active.' const ignore = isGerman ? 'Wenn Sie diese Änderung nicht angefordert haben, können Sie diese E-Mail ignorieren — es wird nichts geändert.' : 'If you did not request this change, you can ignore this e-mail — nothing will be changed.' return `

${title}

${intro}

${fieldLbl}${escapeHtml(fieldLabel)}
${oldLbl}${escapeHtml(oldEmail) || '—'}
${newLbl}${escapeHtml(newEmail)}

${cta}

${expiry}

${ignore}

` } /** * Sends the confirmation link to the NEW address. Throws on transport * failure — the caller must then discard the pending row so the UI doesn't * show a "waiting for confirmation" that can never arrive. */ export const sendVerificationEmail = async (opts: { fieldKey: string oldEmail: string newEmail: string token: string language: string }) => { const def = EMAIL_FIELDS[opts.fieldKey] if (!def) throw new Error(`Unknown email field: ${opts.fieldKey}`) const isGerman = String(opts.language || 'de_DE').startsWith('de') const label = isGerman ? def.labelDe : def.labelEn const link = `${APP_BASE_URL}/verify-email?token=${encodeURIComponent(opts.token)}` const transporter = nodemailer.createTransport({ host: 'localhost', port: 25, secure: false, tls: { rejectUnauthorized: false } }) await transporter.sendMail({ from: 'no-reply@logyou.de', to: opts.newEmail, subject: isGerman ? `Bitte bestätigen Sie Ihre neue E-Mail-Adresse (${label})` : `Please confirm your new e-mail address (${label})`, html: buildVerificationHtml(label, opts.oldEmail, opts.newEmail, link, isGerman) }) }