Files

88 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}