first commit
This commit is contained in:
13
lib/format.ts
Normal file
13
lib/format.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Byte cinsinden değeri okunabilir formata çevirir.
|
||||
* Örn: 3221225472 → "3 GB", 524288000 → "500 MB"
|
||||
*/
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 MB";
|
||||
const mb = bytes / (1024 * 1024);
|
||||
if (mb >= 1024) {
|
||||
const gb = mb / 1024;
|
||||
return gb % 1 === 0 ? `${gb} GB` : `${gb.toFixed(1)} GB`;
|
||||
}
|
||||
return mb % 1 === 0 ? `${mb} MB` : `${mb.toFixed(1)} MB`;
|
||||
}
|
||||
339
lib/imap.ts
Normal file
339
lib/imap.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* lib/imap.ts
|
||||
* IMAP client using imapflow — server-side only.
|
||||
* Connects with user's mailbox credentials (from session), never stored in DB.
|
||||
*/
|
||||
|
||||
import { ImapFlow } from "imapflow";
|
||||
import { simpleParser, ParsedMail } from "mailparser";
|
||||
|
||||
const IMAP_HOST = process.env.MAILCOW_API_URL
|
||||
? new URL(process.env.MAILCOW_API_URL).hostname
|
||||
: "localhost";
|
||||
const IMAP_PORT = parseInt(process.env.IMAP_PORT ?? "993");
|
||||
|
||||
export interface MailCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface MailFolder {
|
||||
name: string;
|
||||
path: string;
|
||||
specialUse?: string;
|
||||
messages: number;
|
||||
unseen: number;
|
||||
}
|
||||
|
||||
export interface MailEnvelope {
|
||||
uid: number;
|
||||
seq: number;
|
||||
subject: string;
|
||||
from: { name: string; address: string }[];
|
||||
to: { name: string; address: string }[];
|
||||
date: string;
|
||||
seen: boolean;
|
||||
flagged: boolean;
|
||||
hasAttachments: boolean;
|
||||
size: number;
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export interface MailMessage {
|
||||
uid: number;
|
||||
subject: string;
|
||||
from: { name: string; address: string }[];
|
||||
to: { name: string; address: string }[];
|
||||
cc: { name: string; address: string }[];
|
||||
date: string;
|
||||
html: string;
|
||||
text: string;
|
||||
seen: boolean;
|
||||
flagged: boolean;
|
||||
attachments: {
|
||||
filename: string;
|
||||
contentType: string;
|
||||
size: number;
|
||||
cid?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Create an IMAP connection for the given credentials */
|
||||
async function createConnection(creds: MailCredentials): Promise<ImapFlow> {
|
||||
const client = new ImapFlow({
|
||||
host: IMAP_HOST,
|
||||
port: IMAP_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: creds.email,
|
||||
pass: creds.password,
|
||||
},
|
||||
logger: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
/** List all mailbox folders */
|
||||
export async function listFolders(creds: MailCredentials): Promise<MailFolder[]> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const mailboxes = await client.list();
|
||||
const folders: MailFolder[] = [];
|
||||
|
||||
for (const mb of mailboxes) {
|
||||
try {
|
||||
const status = await client.status(mb.path, {
|
||||
messages: true,
|
||||
unseen: true,
|
||||
});
|
||||
folders.push({
|
||||
name: mb.name,
|
||||
path: mb.path,
|
||||
specialUse: mb.specialUse || undefined,
|
||||
messages: status.messages ?? 0,
|
||||
unseen: status.unseen ?? 0,
|
||||
});
|
||||
} catch {
|
||||
folders.push({
|
||||
name: mb.name,
|
||||
path: mb.path,
|
||||
specialUse: mb.specialUse || undefined,
|
||||
messages: 0,
|
||||
unseen: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return folders;
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/** Map imapflow address to our format */
|
||||
function mapAddr(
|
||||
addrs: { name?: string; address?: string }[] | undefined
|
||||
): { name: string; address: string }[] {
|
||||
if (!addrs) return [];
|
||||
return addrs.map((a) => ({
|
||||
name: a.name ?? "",
|
||||
address: a.address ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
/** List messages in a folder (paginated) */
|
||||
export async function listMessages(
|
||||
creds: MailCredentials,
|
||||
folder: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 50
|
||||
): Promise<{ messages: MailEnvelope[]; total: number }> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const lock = await client.getMailboxLock(folder);
|
||||
try {
|
||||
const total = client.mailbox?.exists ?? 0;
|
||||
if (total === 0) return { messages: [], total: 0 };
|
||||
|
||||
// Newest first: fetch from end
|
||||
const end = total;
|
||||
const start = Math.max(1, end - (page * pageSize) + 1);
|
||||
const fetchStart = Math.max(1, end - ((page - 1) * pageSize));
|
||||
const fetchEnd = Math.max(1, end - (page * pageSize) + 1);
|
||||
const range = `${fetchEnd}:${fetchStart}`;
|
||||
|
||||
const messages: MailEnvelope[] = [];
|
||||
for await (const msg of client.fetch(range, {
|
||||
uid: true,
|
||||
envelope: true,
|
||||
flags: true,
|
||||
bodyStructure: true,
|
||||
size: true,
|
||||
})) {
|
||||
const env = msg.envelope;
|
||||
messages.push({
|
||||
uid: msg.uid,
|
||||
seq: msg.seq,
|
||||
subject: env.subject ?? "(Konu yok)",
|
||||
from: mapAddr(env.from),
|
||||
to: mapAddr(env.to),
|
||||
date: env.date?.toISOString() ?? new Date().toISOString(),
|
||||
seen: msg.flags?.has("\\Seen") ?? false,
|
||||
flagged: msg.flags?.has("\\Flagged") ?? false,
|
||||
hasAttachments: hasAttachmentParts(msg.bodyStructure),
|
||||
size: msg.size ?? 0,
|
||||
preview: "",
|
||||
});
|
||||
}
|
||||
|
||||
// Reverse so newest is first
|
||||
messages.reverse();
|
||||
return { messages, total };
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if bodyStructure has attachment parts */
|
||||
function hasAttachmentParts(structure: any): boolean {
|
||||
if (!structure) return false;
|
||||
if (structure.disposition === "attachment") return true;
|
||||
if (structure.childNodes) {
|
||||
return structure.childNodes.some((n: any) => hasAttachmentParts(n));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Fetch a single message by UID with full body */
|
||||
export async function getMessage(
|
||||
creds: MailCredentials,
|
||||
folder: string,
|
||||
uid: number
|
||||
): Promise<MailMessage | null> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const lock = await client.getMailboxLock(folder);
|
||||
try {
|
||||
const raw = await client.download(String(uid), undefined, { uid: true });
|
||||
if (!raw?.content) return null;
|
||||
|
||||
const parsed: ParsedMail = await simpleParser(raw.content);
|
||||
|
||||
// Mark as seen
|
||||
await client.messageFlagsAdd(String(uid), ["\\Seen"], { uid: true });
|
||||
|
||||
return {
|
||||
uid,
|
||||
subject: parsed.subject ?? "(Konu yok)",
|
||||
from: parsed.from?.value?.map((a) => ({ name: a.name ?? "", address: a.address ?? "" })) ?? [],
|
||||
to: (Array.isArray(parsed.to) ? parsed.to : parsed.to ? [parsed.to] : [])
|
||||
.flatMap((t) => t.value?.map((a) => ({ name: a.name ?? "", address: a.address ?? "" })) ?? []),
|
||||
cc: (Array.isArray(parsed.cc) ? parsed.cc : parsed.cc ? [parsed.cc] : [])
|
||||
.flatMap((c) => c.value?.map((a) => ({ name: a.name ?? "", address: a.address ?? "" })) ?? []),
|
||||
date: parsed.date?.toISOString() ?? new Date().toISOString(),
|
||||
html: parsed.html || "",
|
||||
text: parsed.text || "",
|
||||
seen: true,
|
||||
flagged: false,
|
||||
attachments: (parsed.attachments ?? []).map((att) => ({
|
||||
filename: att.filename ?? "unnamed",
|
||||
contentType: att.contentType ?? "application/octet-stream",
|
||||
size: att.size ?? 0,
|
||||
cid: att.cid,
|
||||
})),
|
||||
};
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/** Download attachment by filename */
|
||||
export async function getAttachment(
|
||||
creds: MailCredentials,
|
||||
folder: string,
|
||||
uid: number,
|
||||
filename: string
|
||||
): Promise<{ content: Buffer; contentType: string } | null> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const lock = await client.getMailboxLock(folder);
|
||||
try {
|
||||
const raw = await client.download(String(uid), undefined, { uid: true });
|
||||
if (!raw?.content) return null;
|
||||
|
||||
const parsed = await simpleParser(raw.content);
|
||||
const att = parsed.attachments?.find((a) => a.filename === filename);
|
||||
if (!att) return null;
|
||||
|
||||
return {
|
||||
content: att.content,
|
||||
contentType: att.contentType ?? "application/octet-stream",
|
||||
};
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/** Move message to another folder */
|
||||
export async function moveMessage(
|
||||
creds: MailCredentials,
|
||||
fromFolder: string,
|
||||
uid: number,
|
||||
toFolder: string
|
||||
): Promise<void> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const lock = await client.getMailboxLock(fromFolder);
|
||||
try {
|
||||
await client.messageMove(String(uid), toFolder, { uid: true });
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete message (move to Trash or permanent delete) */
|
||||
export async function deleteMessage(
|
||||
creds: MailCredentials,
|
||||
folder: string,
|
||||
uid: number
|
||||
): Promise<void> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const lock = await client.getMailboxLock(folder);
|
||||
try {
|
||||
// If already in Trash, permanently delete
|
||||
if (folder.toLowerCase().includes("trash")) {
|
||||
await client.messageFlagsAdd(String(uid), ["\\Deleted"], { uid: true });
|
||||
await client.expunge();
|
||||
} else {
|
||||
// Move to Trash
|
||||
await client.messageMove(String(uid), "Trash", { uid: true });
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle flag on a message */
|
||||
export async function toggleFlag(
|
||||
creds: MailCredentials,
|
||||
folder: string,
|
||||
uid: number,
|
||||
flag: string,
|
||||
add: boolean
|
||||
): Promise<void> {
|
||||
const client = await createConnection(creds);
|
||||
try {
|
||||
const lock = await client.getMailboxLock(folder);
|
||||
try {
|
||||
if (add) {
|
||||
await client.messageFlagsAdd(String(uid), [flag], { uid: true });
|
||||
} else {
|
||||
await client.messageFlagsRemove(String(uid), [flag], { uid: true });
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
56
lib/mail-session.ts
Normal file
56
lib/mail-session.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* lib/mail-session.ts
|
||||
* Stores/retrieves mail credentials in an encrypted httpOnly cookie.
|
||||
* Credentials never hit the database.
|
||||
*/
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const COOKIE_NAME = "ayrismail_creds";
|
||||
|
||||
export interface MailSessionData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode credentials to base64 (in production, use proper encryption
|
||||
* with AES-256-GCM and AUTH_SECRET as key).
|
||||
*/
|
||||
function encode(data: MailSessionData): string {
|
||||
return Buffer.from(JSON.stringify(data)).toString("base64");
|
||||
}
|
||||
|
||||
function decode(token: string): MailSessionData | null {
|
||||
try {
|
||||
return JSON.parse(Buffer.from(token, "base64").toString("utf-8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Save mail credentials to cookie */
|
||||
export async function setMailSession(data: MailSessionData): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(COOKIE_NAME, encode(data), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
/** Get mail credentials from cookie */
|
||||
export async function getMailSession(): Promise<MailSessionData | null> {
|
||||
const cookieStore = await cookies();
|
||||
const cookie = cookieStore.get(COOKIE_NAME);
|
||||
if (!cookie?.value) return null;
|
||||
return decode(cookie.value);
|
||||
}
|
||||
|
||||
/** Clear mail credentials cookie */
|
||||
export async function clearMailSession(): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(COOKIE_NAME);
|
||||
}
|
||||
211
lib/mailcow.ts
Normal file
211
lib/mailcow.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* lib/mailcow.ts
|
||||
* Mailcow API client — server-side only.
|
||||
* Uses the single super-admin API key from .env.
|
||||
*/
|
||||
|
||||
const BASE = process.env.MAILCOW_API_URL?.replace(/\/$/, "") ?? "";
|
||||
const KEY = process.env.MAILCOW_API_KEY ?? "";
|
||||
|
||||
async function mfetch(path: string, options: RequestInit = {}) {
|
||||
const url = `${BASE}/api/v1${path}`;
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": KEY,
|
||||
...options.headers,
|
||||
},
|
||||
// Don't cache — always fresh
|
||||
cache: "no-store",
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────
|
||||
|
||||
export interface MailcowDomain {
|
||||
domain_name: string;
|
||||
description: string;
|
||||
active: string; // "1" | "0"
|
||||
mboxes_in_domain: number;
|
||||
mboxes_left: number;
|
||||
max_num_mboxes_for_domain: number;
|
||||
quota_used_in_domain: string;
|
||||
max_quota_for_domain: number;
|
||||
max_quota_for_mbox: number;
|
||||
aliases_in_domain: number;
|
||||
aliases_left: number;
|
||||
}
|
||||
|
||||
export interface MailcowMailbox {
|
||||
username: string; // full email e.g. info@domain.com
|
||||
name: string; // display name
|
||||
local_part: string;
|
||||
domain: string;
|
||||
quota: number; // bytes
|
||||
quota_used: number; // bytes
|
||||
active: string; // "1" | "0"
|
||||
created: string;
|
||||
modified: string;
|
||||
}
|
||||
|
||||
export interface MailcowDomainAdmin {
|
||||
username: string;
|
||||
active: string;
|
||||
domains: string[];
|
||||
created: string;
|
||||
modified: string;
|
||||
}
|
||||
|
||||
// ─── Domains ────────────────────────────────────────────────
|
||||
|
||||
export async function getDomains(): Promise<MailcowDomain[]> {
|
||||
const res = await mfetch("/get/domain/all");
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
export async function getDomain(domain: string): Promise<MailcowDomain | null> {
|
||||
const res = await mfetch(`/get/domain/${domain}`);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data[0] ?? null : data ?? null;
|
||||
}
|
||||
|
||||
export async function createDomain(payload: {
|
||||
domain: string;
|
||||
description?: string;
|
||||
aliases?: number;
|
||||
mailboxes?: number;
|
||||
defquota?: number;
|
||||
maxquota?: number;
|
||||
quota?: number;
|
||||
active?: number;
|
||||
}) {
|
||||
const body = {
|
||||
active: 1,
|
||||
aliases: 400,
|
||||
mailboxes: 10,
|
||||
defquota: 3072,
|
||||
maxquota: 10240,
|
||||
quota: 10240,
|
||||
...payload,
|
||||
};
|
||||
const res = await mfetch("/add/domain", { method: "POST", body: JSON.stringify(body) });
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
export async function deleteDomain(domain: string) {
|
||||
const res = await mfetch("/delete/domain", {
|
||||
method: "POST",
|
||||
body: JSON.stringify([domain]),
|
||||
});
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
// ─── Domain Admins ──────────────────────────────────────────
|
||||
|
||||
export async function getDomainAdmins(): Promise<MailcowDomainAdmin[]> {
|
||||
const res = await mfetch("/get/domain-admin/all");
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : Object.values(data);
|
||||
}
|
||||
|
||||
export async function createDomainAdmin(payload: {
|
||||
username: string;
|
||||
password: string;
|
||||
domains: string; // comma-separated or single domain
|
||||
active?: number;
|
||||
}) {
|
||||
const body = {
|
||||
active: 1,
|
||||
password2: payload.password,
|
||||
...payload,
|
||||
};
|
||||
const res = await mfetch("/add/domain-admin", { method: "POST", body: JSON.stringify(body) });
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
export async function deleteDomainAdmin(username: string) {
|
||||
const res = await mfetch("/delete/domain-admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify([username]),
|
||||
});
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
// ─── Mailboxes ──────────────────────────────────────────────
|
||||
|
||||
export async function getMailboxes(domain: string): Promise<MailcowMailbox[]> {
|
||||
const res = await mfetch(`/get/mailbox/all/${domain}`);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
export async function createMailbox(payload: {
|
||||
local_part: string;
|
||||
domain: string;
|
||||
name: string;
|
||||
password: string;
|
||||
quota?: number;
|
||||
active?: number;
|
||||
}) {
|
||||
const body = {
|
||||
quota: 3072,
|
||||
active: 1,
|
||||
password2: payload.password,
|
||||
...payload,
|
||||
};
|
||||
const res = await mfetch("/add/mailbox", { method: "POST", body: JSON.stringify(body) });
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
export async function deleteMailbox(emails: string[]) {
|
||||
const res = await mfetch("/delete/mailbox", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(emails),
|
||||
});
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
export async function editMailbox(
|
||||
emails: string[],
|
||||
attr: {
|
||||
password?: string;
|
||||
password2?: string;
|
||||
active?: number;
|
||||
quota?: number;
|
||||
name?: string;
|
||||
}
|
||||
) {
|
||||
const body = { items: emails, attr };
|
||||
const res = await mfetch("/edit/mailbox", { method: "POST", body: JSON.stringify(body) });
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
// ─── Status ─────────────────────────────────────────────────
|
||||
|
||||
export async function getVersionStatus() {
|
||||
const res = await mfetch("/get/status/version");
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ─── DKIM ────────────────────────────────────────────────────
|
||||
|
||||
export async function getDKIM(domain: string) {
|
||||
const res = await mfetch(`/get/dkim/${domain}`);
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
}
|
||||
131
lib/smtp.ts
Normal file
131
lib/smtp.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* lib/smtp.ts
|
||||
* SMTP mail sending via nodemailer + IMAP Sent folder append.
|
||||
* Uses user's mailbox credentials for authenticated SMTP.
|
||||
*/
|
||||
|
||||
import nodemailer from "nodemailer";
|
||||
import { ImapFlow } from "imapflow";
|
||||
import type { MailCredentials } from "./imap";
|
||||
|
||||
const SMTP_HOST = process.env.MAILCOW_API_URL
|
||||
? new URL(process.env.MAILCOW_API_URL).hostname
|
||||
: "localhost";
|
||||
const SMTP_PORT = parseInt(process.env.SMTP_PORT ?? "587");
|
||||
const IMAP_HOST = SMTP_HOST;
|
||||
const IMAP_PORT = parseInt(process.env.IMAP_PORT ?? "993");
|
||||
|
||||
export interface SendMailOptions {
|
||||
to: string;
|
||||
cc?: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text?: string;
|
||||
inReplyTo?: string;
|
||||
references?: string;
|
||||
attachments?: {
|
||||
filename: string;
|
||||
content: Buffer | string;
|
||||
contentType?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export async function sendMail(
|
||||
creds: MailCredentials,
|
||||
options: SendMailOptions
|
||||
): Promise<{ success: boolean; messageId?: string; error?: string }> {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
secure: SMTP_PORT === 465,
|
||||
auth: {
|
||||
user: creds.email,
|
||||
pass: creds.password,
|
||||
},
|
||||
tls: {
|
||||
// Mailcow self-signed cert'e izin ver
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
from: creds.email,
|
||||
to: options.to,
|
||||
cc: options.cc || undefined,
|
||||
subject: options.subject,
|
||||
html: options.html,
|
||||
text: options.text || undefined,
|
||||
inReplyTo: options.inReplyTo || undefined,
|
||||
references: options.references || undefined,
|
||||
attachments: options.attachments?.map((a) => ({
|
||||
filename: a.filename,
|
||||
content: a.content,
|
||||
contentType: a.contentType,
|
||||
})),
|
||||
};
|
||||
|
||||
const result = await transporter.sendMail(mailOptions);
|
||||
|
||||
// Gönderilen maili Sent klasörüne kaydet (IMAP APPEND)
|
||||
try {
|
||||
await appendToSent(creds, mailOptions);
|
||||
} catch (e) {
|
||||
// Sent'e kaydetme başarısız olursa mail yine de gitmiş olur
|
||||
console.error("Sent klasörüne kaydetme hatası:", e);
|
||||
}
|
||||
|
||||
return { success: true, messageId: result.messageId };
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err?.message ?? "SMTP hatası" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gönderilen maili IMAP Sent klasörüne kaydet.
|
||||
* Mailcow'da Sent klasörü "Sent" olarak adlandırılır.
|
||||
*/
|
||||
async function appendToSent(
|
||||
creds: MailCredentials,
|
||||
mailOptions: Record<string, any>
|
||||
): Promise<void> {
|
||||
// nodemailer ile raw mesaj oluştur
|
||||
const transporter = nodemailer.createTransport({ jsonTransport: true });
|
||||
const compiled = await transporter.sendMail(mailOptions);
|
||||
const rawMessage = JSON.parse(compiled.message);
|
||||
|
||||
// Gerçek raw RFC822 mesajı oluştur
|
||||
const buildTransporter = nodemailer.createTransport({ streamTransport: true });
|
||||
const built = await buildTransporter.sendMail(mailOptions);
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of built.message as any) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
const rawBuffer = Buffer.concat(chunks);
|
||||
|
||||
const client = new ImapFlow({
|
||||
host: IMAP_HOST,
|
||||
port: IMAP_PORT,
|
||||
secure: true,
|
||||
auth: { user: creds.email, pass: creds.password },
|
||||
logger: false,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
try {
|
||||
// Sent klasörünü bul
|
||||
const mailboxes = await client.list();
|
||||
let sentPath = "Sent";
|
||||
for (const mb of mailboxes) {
|
||||
if (mb.specialUse === "\\Sent") {
|
||||
sentPath = mb.path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Mesajı Sent klasörüne ekle (Seen flag ile)
|
||||
await client.append(sentPath, rawBuffer, ["\\Seen"]);
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
69
lib/users.ts
Normal file
69
lib/users.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* lib/users.ts
|
||||
* Reads user config from environment variables — no database needed.
|
||||
*
|
||||
* .env format:
|
||||
* USER_0_NAME="Mustafa Ayris"
|
||||
* USER_0_EMAIL="mustafa@ayristech.com"
|
||||
* USER_0_PASSWORD="mustafa123"
|
||||
* USER_0_ROLE="SUPER_ADMIN" // or "DOMAIN_ADMIN"
|
||||
* USER_0_DOMAINS="*" // "*" for all, or "domain1.com,domain2.com"
|
||||
*
|
||||
* USER_1_NAME="Emina Karabudak"
|
||||
* USER_1_EMAIL="emina@ayristech.com"
|
||||
* USER_1_PASSWORD="emina123"
|
||||
* USER_1_ROLE="DOMAIN_ADMIN"
|
||||
* USER_1_DOMAINS="aveminakarabudak.com"
|
||||
*/
|
||||
|
||||
export interface AppUser {
|
||||
id: string; // "user_0", "user_1", ...
|
||||
name: string;
|
||||
email: string;
|
||||
password: string; // plain text — store hashed in prod or use secrets manager
|
||||
role: "SUPER_ADMIN" | "DOMAIN_ADMIN";
|
||||
domains: string[]; // ["*"] for super admin, ["domain.com"] for domain admins
|
||||
}
|
||||
|
||||
/** Load all users defined in environment variables */
|
||||
export function getUsers(): AppUser[] {
|
||||
const users: AppUser[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const name = process.env[`USER_${i}_NAME`];
|
||||
const email = process.env[`USER_${i}_EMAIL`];
|
||||
const password = process.env[`USER_${i}_PASSWORD`];
|
||||
const role = process.env[`USER_${i}_ROLE`] as AppUser["role"];
|
||||
const domainsRaw = process.env[`USER_${i}_DOMAINS`] ?? "";
|
||||
|
||||
if (!name || !email || !password) break;
|
||||
|
||||
users.push({
|
||||
id: `user_${i}`,
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
role: role ?? "DOMAIN_ADMIN",
|
||||
domains: domainsRaw === "*" ? ["*"] : domainsRaw.split(",").map((d) => d.trim()).filter(Boolean),
|
||||
});
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/** Find user by email and validate password */
|
||||
export function authenticateUser(email: string, password: string): AppUser | null {
|
||||
const users = getUsers();
|
||||
const user = users.find((u) => u.email.toLowerCase() === email.toLowerCase());
|
||||
if (!user) return null;
|
||||
if (user.password !== password) return null;
|
||||
return user;
|
||||
}
|
||||
|
||||
/** Check if a user has access to a specific domain */
|
||||
export function canAccessDomain(userDomains: string[], domain: string): boolean {
|
||||
return userDomains.includes("*") || userDomains.includes(domain);
|
||||
}
|
||||
Reference in New Issue
Block a user