/** * Resend e-posta gönderimi — bağımlılık yok (Node 18+ built-in fetch). * * Env: * RESEND_API_KEY — Resend API anahtarı (re_...). Yoksa e-posta kapalıdır. * EMAIL_FROM — Gönderen adres. Üretimde Resend'de DNS (SPF/DKIM) ile * doğrulanmış kendi domaininiz olmalı. Doğrulanana kadar * Resend'in paylaşımlı test adresi kullanılır. * * Kaynak/Endpoint: POST https://api.resend.com/emails (Bearer auth) */ const RESEND_ENDPOINT = 'https://api.resend.com/emails'; // Resend anahtarları 're_' ile başlar. Yer tutucu/boş değerler yapılandırılmış // sayılmaz — aksi halde arayüz "aktif" der ama gönderim hata verir. export function emailConfigured() { const k = (process.env.RESEND_API_KEY || '').trim(); return k.startsWith('re_') && k.length > 10; } export function emailFrom() { return process.env.EMAIL_FROM || 'İhaleist '; } export async function sendEmail({ to, subject, html, text, from = emailFrom(), idempotencyKey }) { const key = (process.env.RESEND_API_KEY || '').trim(); if (!emailConfigured()) { throw new Error("E-posta yapılandırılmamış — Coolify'da RESEND_API_KEY değerine gerçek anahtarı (re_...) yapıştırın."); } const recipients = Array.isArray(to) ? to : [to]; if (!recipients.length || recipients.length > 50 || recipients.some(value => typeof value !== 'string' || !value.trim() || value.length > 320)) throw new Error('Alıcı listesi geçersiz veya çok büyük.'); if (typeof subject !== 'string' || !subject.trim() || subject.length > 500 || (html != null && typeof html !== 'string') || (text != null && typeof text !== 'string') || (!html && !text) || (html != null && html.length > 2 * 1024 * 1024) || (text != null && text.length > 2 * 1024 * 1024)) throw new Error('E-posta konusu veya içeriği geçersiz.'); if(idempotencyKey!==undefined && (typeof idempotencyKey!=='string'||!idempotencyKey||idempotencyKey.length>256||/[\r\n]/.test(idempotencyKey)))throw new Error('Geçersiz e-posta istek kimliği.'); const res = await fetch(RESEND_ENDPOINT, { signal:AbortSignal.timeout(30000), method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', ...(idempotencyKey?{'Idempotency-Key':idempotencyKey}:{}), }, body: JSON.stringify({ from, to: recipients.map(value => value.trim()), subject: subject.trim(), html, text }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data?.message || `Resend hatası: HTTP ${res.status}`); } if(typeof data.id!=='string'||!data.id.trim())throw new Error('E-posta sağlayıcısı gönderim kimliği döndürmedi; gönderim sonucu belirsiz.'); return { id: data.id }; } // İhale bildirimi için basit, markasız HTML gövdesi export function tenderDigestHtml({ name, tenders }) { const rows = (tenders || []) .map( (t) => ` ${escapeHtml(t.baslik || '')} ${escapeHtml(t.sehir || '')} ${escapeHtml(t.tarih || '')} ` ) .join(''); return `

İhaleist

Merhaba ${escapeHtml(name || '')}, takip kriterlerinize uyan ihaleler:

${rows || ''}
İhale İl Tarih
Yeni ihale yok.

Bu e-posta İhaleist tarafından gönderildi. Bildirim tercihlerinizi uygulama ayarlarından değiştirebilirsiniz.

`; } export function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }