/** * Outbound staff → customer e-mail (lead mail composer, AI-draft send). * * One place for the branded signature (table layout → renders in Outlook / Gmail / * Apple Mail), the plain-text → HTML conversion and the local-MTA transport, so every * "send from the lead page" route mails the same way: * From = info@logyou.de with the sender's display name * Reply-To = the sending user's own AD_User address (answers land with the author) * Body = the user's text (ends at the greeting) + the server-side signature block. * The dev Mac has no MTA → sending fails soft there; never test-send from dev. */ import nodemailer from 'nodemailer' import fetchHelper from '../fetchHelper' import { isValidEmail } from './leadMailbox' import { APP_BASE_URL } from '../emailVerification' export const STAFF_MAIL_FROM = 'info@logyou.de' export const MAX_SUBJECT = 255 export const MAX_BODY = 20000 const NAVY = '#1c324b' const ORANGE = '#F15924' const LOGO_URL = `${APP_BASE_URL}/assets/logyou-logo-color.png` export const COMPANY = { name: 'LogYou GmbH', street: 'Mühlenweg 4', city: '35510 Butzbach', web: 'logyou.de', mail: STAFF_MAIL_FROM, legal: 'Geschäftsführer: Dennis Montag · Amtsgericht Friedberg, HRB 10595' } /** `phone` = AD_User.Phone (direct line), `phone2` = AD_User.Phone2 (mobile) — both optional. */ export interface Sender { name: string; title: string; email: string; phone: string; phone2: string } const escapeHtml = (s: string) => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') /** The signature's sender block from the logged-in user's AD_User record (fail-soft). */ export const loadSender = async (event: any, token: string, userId: number): Promise => { const sender: Sender = { name: '', title: '', email: '', phone: '', phone2: '' } try { const me: any = await fetchHelper(event, `models/ad_user/${userId}`, 'GET', token, null) sender.name = String(me?.Name || me?.name || '') sender.email = String(me?.EMail || me?.eMail || '').trim() sender.phone = String(me?.Phone || me?.phone || '').trim() sender.phone2 = String(me?.Phone2 || me?.phone2 || '').trim() // Only a second number that differs from the first earns its own line. if (sender.phone2 && sender.phone2.replace(/[^\d+]/g, '') === sender.phone.replace(/[^\d+]/g, '')) sender.phone2 = '' sender.title = String(me?.Title || me?.title || me?.C_Job_ID?.identifier || '').trim() } catch { /* signature falls back to the company block */ } return sender } /** Branded signature: orange rule, navy name, T/M/E/W lines, logo right, legal footer. */ export const signatureHtml = (sender: Sender, lang: string) => { const en = lang === 'en' const line = (icon: string, html: string) => `${icon}${html}` const rows: string[] = [] const tel = (n: string) => `${escapeHtml(n)}` if (sender.phone) rows.push(line('T', tel(sender.phone))) if (sender.phone2) rows.push(line('M', tel(sender.phone2))) if (sender.email) rows.push(line('E', `${escapeHtml(sender.email)}`)) rows.push(line('W', `${COMPANY.web}`)) return `
 
${escapeHtml(sender.name || COMPANY.name)}
${sender.title ? `
${escapeHtml(sender.title)}
` : ''}
${COMPANY.name}
${rows.join('')}
${COMPANY.name}
${COMPANY.name} · ${COMPANY.street} · ${COMPANY.city}${en ? ', Germany' : ''} · ${escapeHtml(COMPANY.legal)}
${en ? 'Fulfillment · Warehousing · Shipping for online retailers' : 'Fulfillment · Lagerung · Versand für Online-Händler'}
` } export const signatureText = (sender: Sender) => [ '', '--', sender.name || COMPANY.name, ...(sender.title ? [sender.title] : []), COMPANY.name, ...(sender.phone ? [`T ${sender.phone}`] : []), ...(sender.phone2 ? [`M ${sender.phone2}`] : []), ...(sender.email ? [`E ${sender.email}`] : []), `W https://${COMPANY.web}`, '', `${COMPANY.name} · ${COMPANY.street} · ${COMPANY.city} · ${COMPANY.legal}` ].join('\n') /** Plain text (paragraphs separated by blank lines) → branded HTML mail incl. signature. */ export const textToHtml = (text: string, sender: Sender, lang: string) => `
${text.split(/\n{2,}/).map((p) => `

${escapeHtml(p).replace(/\n/g, '
')}

`).join('')} ${signatureHtml(sender, lang)}
` export interface StaffMailInput { sender: Sender to: string toName?: string cc?: string[] subject: string text: string lang?: string } /** Send via the local MTA. Throws on transport errors (callers answer 502). */ export const sendStaffMail = async (input: StaffMailInput) => { const senderName = String(input.sender?.name || '').replace(/"/g, '') const toName = String(input.toName || '').replace(/"/g, '') const cc = (input.cc || []).filter((s) => isValidEmail(s)) const lang = input.lang === 'en' ? 'en' : 'de' const transporter = nodemailer.createTransport({ host: 'localhost', port: 25, secure: false, tls: { rejectUnauthorized: false } }) await transporter.sendMail({ from: senderName ? `"${senderName} | LogYou" <${STAFF_MAIL_FROM}>` : STAFF_MAIL_FROM, to: toName ? `"${toName}" <${input.to}>` : input.to, cc: cc.length ? cc : undefined, replyTo: isValidEmail(input.sender?.email) ? input.sender.email : undefined, subject: input.subject || (lang === 'en' ? '(no subject)' : '(kein Betreff)'), text: input.text + '\n' + signatureText(input.sender), html: textToHtml(input.text, input.sender, lang) }) return { cc } }