88 lines
4.6 KiB
JavaScript
88 lines
4.6 KiB
JavaScript
/**
|
||
* 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 <onboarding@resend.dev>';
|
||
}
|
||
|
||
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) => `<tr>
|
||
<td style="padding:8px 12px;border-bottom:1px solid #eef1f5;font-size:13px;color:#0e1526">${escapeHtml(t.baslik || '')}</td>
|
||
<td style="padding:8px 12px;border-bottom:1px solid #eef1f5;font-size:12px;color:#586074;white-space:nowrap">${escapeHtml(t.sehir || '')}</td>
|
||
<td style="padding:8px 12px;border-bottom:1px solid #eef1f5;font-size:12px;color:#586074;white-space:nowrap">${escapeHtml(t.tarih || '')}</td>
|
||
</tr>`
|
||
)
|
||
.join('');
|
||
return `<div style="font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto">
|
||
<h2 style="color:#2563eb;margin:0 0 4px">İhaleist</h2>
|
||
<p style="color:#586074;font-size:14px;margin:0 0 16px">Merhaba ${escapeHtml(name || '')}, takip kriterlerinize uyan ihaleler:</p>
|
||
<table style="border-collapse:collapse;width:100%;border:1px solid #eef1f5;border-radius:8px;overflow:hidden">
|
||
<thead><tr style="background:#f6f8fb">
|
||
<th style="padding:8px 12px;text-align:left;font-size:11px;color:#586074">İhale</th>
|
||
<th style="padding:8px 12px;text-align:left;font-size:11px;color:#586074">İl</th>
|
||
<th style="padding:8px 12px;text-align:left;font-size:11px;color:#586074">Tarih</th>
|
||
</tr></thead>
|
||
<tbody>${rows || '<tr><td style="padding:12px;color:#8b93a4;font-size:13px" colspan="3">Yeni ihale yok.</td></tr>'}</tbody>
|
||
</table>
|
||
<p style="color:#8b93a4;font-size:11px;margin-top:16px">Bu e-posta İhaleist tarafından gönderildi. Bildirim tercihlerinizi uygulama ayarlarından değiştirebilirsiniz.</p>
|
||
</div>`;
|
||
}
|
||
|
||
export function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
}
|