Einstellungen-Seite: .env-Werte in DB (app_state), keine .env mehr
Neue /einstellungen-Seite (Zahnrad im Header) mit allen bisherigen .env-Werten (Ollama, E-Mail, CalDAV, REST-API), gespeichert in SQLite (app_state, cfg:-Prefix). Libs lesen per lib/config.js zur Laufzeit statt beim Start -> Aenderungen wirken sofort, kein Neustart. Bestehende .env wird beim ersten Start einmalig migriert. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+13
-6
@@ -1,9 +1,10 @@
|
||||
// Third-party REST API (v1) for the Bewerbungs-Tracker.
|
||||
//
|
||||
// Mounted under /api/v1 in server.js. All endpoints except /health require an
|
||||
// API key (env API_TOKEN) sent in the X-API-Key header. Reuses the server's
|
||||
// existing DB helpers, sanitizer, duplicate guard, generation runner and
|
||||
// attachment directories so behaviour stays consistent with the web UI.
|
||||
// API key (API_TOKEN, editable via /einstellungen) sent in the X-API-Key
|
||||
// header. Reuses the server's existing DB helpers, sanitizer, duplicate guard,
|
||||
// generation runner and attachment directories so behaviour stays consistent
|
||||
// with the web UI.
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
@@ -41,16 +42,22 @@ function createExternalApi(deps) {
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// `apiToken` may be a string (static) or a function () => string (dynamic,
|
||||
// read from the DB on each request so an edit on /einstellungen takes effect
|
||||
// without a restart).
|
||||
const resolveToken = () => (typeof apiToken === 'function' ? apiToken() : apiToken);
|
||||
|
||||
// --- API key auth --------------------------------------------------
|
||||
// /health is public so monitoring tools can probe availability; everything
|
||||
// else returns 401 when the header is missing/wrong or the token isn't set.
|
||||
router.use((req, res, next) => {
|
||||
if (req.path === '/health') return next();
|
||||
if (!apiToken) {
|
||||
return res.status(503).json({ error: 'API-Token nicht konfiguriert (API_TOKEN-Umgebungsvariable fehlt).' });
|
||||
const token = resolveToken();
|
||||
if (!token) {
|
||||
return res.status(503).json({ error: 'API-Token nicht konfiguriert (in den Einstellungen setzen).' });
|
||||
}
|
||||
const provided = req.get('X-API-Key');
|
||||
if (!provided || provided !== apiToken) {
|
||||
if (!provided || provided !== token) {
|
||||
return res.status(401).json({ error: 'Ungültiger oder fehlender API-Key (Header: X-API-Key).' });
|
||||
}
|
||||
next();
|
||||
|
||||
+8
-5
@@ -4,7 +4,7 @@
|
||||
// creates / updates / deletes events (PUT / DELETE with iCalendar VEVENTs).
|
||||
// Auth is HTTP Basic over HTTPS using the same mail account as lib/mailer.
|
||||
//
|
||||
// Config (env):
|
||||
// Config (DB, editable via /einstellungen — see lib/config.js):
|
||||
// CALDAV_URL full URL of the calendar collection (must end with /)
|
||||
// MAIL_USER login (shared with mail)
|
||||
// MAIL_PASSWORD password (shared with mail)
|
||||
@@ -14,15 +14,18 @@
|
||||
// Europe/Berlin. Wall-clock input from the UI is converted with wallToUtc().
|
||||
|
||||
const crypto = require('crypto');
|
||||
const config = require('./config');
|
||||
|
||||
const TZ = 'Europe/Berlin';
|
||||
|
||||
// Read fresh on every call so an edit on the /einstellungen page takes effect
|
||||
// immediately (values live in the DB now, not in process.env/.env).
|
||||
function cfg() {
|
||||
return {
|
||||
url: (process.env.CALDAV_URL || '').trim(),
|
||||
user: process.env.MAIL_USER || '',
|
||||
pass: process.env.MAIL_PASSWORD || '',
|
||||
alarmMin: Number(process.env.CALDAV_ALARM_MIN) || 60,
|
||||
url: (config.get('CALDAV_URL') || '').trim(),
|
||||
user: config.get('MAIL_USER') || '',
|
||||
pass: config.get('MAIL_PASSWORD') || '',
|
||||
alarmMin: Number(config.get('CALDAV_ALARM_MIN')) || 60,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+11
-9
@@ -11,14 +11,12 @@
|
||||
// replies as text, token by token, so the UI can render progressively.
|
||||
|
||||
const promptStore = require('./prompts');
|
||||
const config = require('./config');
|
||||
|
||||
const OLLAMA_HOST = (process.env.OLLAMA_HOST || 'https://ollama.com').replace(/\/+$/, '');
|
||||
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'gpt-oss:120b';
|
||||
const OLLAMA_TIMEOUT_MS = Number(process.env.OLLAMA_TIMEOUT_MS || 300000);
|
||||
const MAX_TOOL_ROUNDS = 4;
|
||||
|
||||
function isConfigured() {
|
||||
return Boolean(process.env.OLLAMA_API_KEY);
|
||||
return Boolean(config.ollama().apiKey);
|
||||
}
|
||||
|
||||
// Stream a single chat completion from Ollama. `messages` = [{role, content}]
|
||||
@@ -28,8 +26,8 @@ function isConfigured() {
|
||||
// fires with each incremental text chunk. Returns { content, toolCalls }.
|
||||
// Aborts cleanly via `signal`.
|
||||
async function streamChat({ system, messages, tools, onToken, temperature = 0.6, signal }) {
|
||||
const apiKey = process.env.OLLAMA_API_KEY;
|
||||
if (!apiKey) throw new Error('OLLAMA_API_KEY ist nicht gesetzt.');
|
||||
const { host: ollamaHost, model: ollamaModel, apiKey } = config.ollama();
|
||||
if (!apiKey) throw new Error('OLLAMA_API_KEY ist nicht gesetzt (unter „Einstellungen“ konfigurieren).');
|
||||
|
||||
const allMessages = [];
|
||||
if (system) allMessages.push({ role: 'system', content: system });
|
||||
@@ -43,7 +41,7 @@ async function streamChat({ system, messages, tools, onToken, temperature = 0.6,
|
||||
}
|
||||
|
||||
const body = {
|
||||
model: OLLAMA_MODEL,
|
||||
model: ollamaModel,
|
||||
stream: true,
|
||||
options: { temperature },
|
||||
messages: allMessages,
|
||||
@@ -52,7 +50,7 @@ async function streamChat({ system, messages, tools, onToken, temperature = 0.6,
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${OLLAMA_HOST}/api/chat`, {
|
||||
res = await fetch(`${ollamaHost}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify(body),
|
||||
@@ -216,4 +214,8 @@ function buildContextPrompt(ctx) {
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
module.exports = { isConfigured, streamChat, runChat, buildContextPrompt, OLLAMA_MODEL, MAX_TOOL_ROUNDS };
|
||||
module.exports = {
|
||||
isConfigured, streamChat, runChat, buildContextPrompt, MAX_TOOL_ROUNDS,
|
||||
// Dynamic so an edit on /einstellungen is reflected without a restart.
|
||||
get OLLAMA_MODEL() { return config.ollama().model; },
|
||||
};
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
// Centralized configuration store.
|
||||
//
|
||||
// Replaces the .env file: all settings live in the SQLite app_state table
|
||||
// (rows prefixed "cfg:") and are editable via the /einstellungen page. The
|
||||
// libs read values through config.get() / config.ollama() at *call* time, so
|
||||
// an edit in the UI takes effect immediately — no restart, no .env file.
|
||||
//
|
||||
// On first start of an install that previously used .env, init() migrates any
|
||||
// still-present env value into the DB once, so existing config is not lost.
|
||||
// After that the database is the single source of truth; process.env is only a
|
||||
// fallback for keys that were never saved (and for the one-time migration).
|
||||
|
||||
const DEFAULTS = {
|
||||
// Ollama Cloud (KI text generation + chat).
|
||||
OLLAMA_API_KEY: '',
|
||||
OLLAMA_MODEL: 'gpt-oss:120b',
|
||||
OLLAMA_HOST: 'https://ollama.com',
|
||||
OLLAMA_TIMEOUT_MS: '300000',
|
||||
// E-Mail (SMTP submission + IMAP receive).
|
||||
MAIL_HOST: '',
|
||||
MAIL_SMTP_PORT: '587',
|
||||
MAIL_IMAP_PORT: '993',
|
||||
MAIL_USER: '',
|
||||
MAIL_PASSWORD: '',
|
||||
MAIL_FROM_NAME: '',
|
||||
MAIL_FROM: '',
|
||||
MAIL_IMAP_MAILBOX: 'INBOX',
|
||||
MAIL_POLL_MS: '180000',
|
||||
// Bewerbungskalender (CalDAV, z. B. SOGo).
|
||||
CALDAV_URL: '',
|
||||
CALDAV_ALARM_MIN: '60',
|
||||
CALDAV_POLL_MS: '300000',
|
||||
// REST-API für Drittanbietersoftware (/api/v1, Header X-API-Key).
|
||||
API_TOKEN: '',
|
||||
};
|
||||
|
||||
// UI metadata for the /einstellungen page: grouped sections with input types.
|
||||
// `secret` fields render as password inputs with a reveal toggle.
|
||||
const FIELDS = [
|
||||
{
|
||||
titel: 'Ollama Cloud (KI-Generierung + Chat)',
|
||||
beschreibung: 'Steuerung der KI, die Bewerbungsunterlagen erzeugt und den KI-Chat beantwortet. Ohne API-Schlüssel sind diese Funktionen deaktiviert.',
|
||||
items: [
|
||||
{ key: 'OLLAMA_API_KEY', label: 'API-Schlüssel', secret: true, help: 'Schlüssel von https://ollama.com/settings/keys' },
|
||||
{ key: 'OLLAMA_MODEL', label: 'Modell', help: 'Standard: gpt-oss:120b' },
|
||||
{ key: 'OLLAMA_HOST', label: 'Host', help: 'Standard: https://ollama.com — lokaler Ollama: http://localhost:11434' },
|
||||
{ key: 'OLLAMA_TIMEOUT_MS', label: 'Timeout (ms)', help: 'Standard: 300000 (5 Min.)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: 'E-Mail (SMTP-Versand + IMAP-Empfang)',
|
||||
beschreibung: 'Versand läuft über den eigenen Mailserver (DKIM/SPF/DMARC-Alignment). Ohne Host/Benutzer/Passwort ist der E-Mail-Teil deaktiviert.',
|
||||
items: [
|
||||
{ key: 'MAIL_HOST', label: 'SMTP/IMAP Host' },
|
||||
{ key: 'MAIL_SMTP_PORT', label: 'SMTP-Port', help: '587 = STARTTLS, 465 = implicit TLS' },
|
||||
{ key: 'MAIL_IMAP_PORT', label: 'IMAP-Port', help: 'Standard: 993 (implicit TLS)' },
|
||||
{ key: 'MAIL_USER', label: 'Benutzername (Login)', help: 'Auch CalDAV-Login' },
|
||||
{ key: 'MAIL_PASSWORD', label: 'Passwort', secret: true, help: 'Auch CalDAV-Passwort' },
|
||||
{ key: 'MAIL_FROM_NAME', label: 'Absendername' },
|
||||
{ key: 'MAIL_FROM', label: 'Absenderadresse', help: 'Leer = Benutzername' },
|
||||
{ key: 'MAIL_IMAP_MAILBOX', label: 'IMAP-Postfach', help: 'Standard: INBOX' },
|
||||
{ key: 'MAIL_POLL_MS', label: 'Abrufintervall (ms)', help: 'Standard: 180000 (3 Min.) — greift nach Neustart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: 'Bewerbungskalender (CalDAV)',
|
||||
beschreibung: 'Voll-URL der Kalender-Sammlung (mit abschließendem /). Authentifizierung läuft über MAIL_USER/MAIL_PASSWORD. Ohne URL sind die Kalender-Funktionen deaktiviert.',
|
||||
items: [
|
||||
{ key: 'CALDAV_URL', label: 'Kalender-URL', help: 'z. B. https://mail.example.com/SOGo/dav/name@…/Calendar/XXXX/' },
|
||||
{ key: 'CALDAV_ALARM_MIN', label: 'Erinnerung (Minuten vor Termin)', help: 'Standard: 60' },
|
||||
{ key: 'CALDAV_POLL_MS', label: 'Sync-Intervall (ms)', help: 'Standard: 300000 (5 Min.) — greift nach Neustart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: 'REST-API für Drittanbietersoftware',
|
||||
beschreibung: 'Ist ein Token gesetzt, ist /api/v1 aktiv und erwartet den Wert im Header „X-API-Key“. Ohne Token antwortet die API (bis auf /health) mit 503. Swagger unter /swagger.',
|
||||
items: [
|
||||
{ key: 'API_TOKEN', label: 'API-Token (X-API-Key)', secret: true, help: 'Leer = API deaktiviert' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const PREFIX = 'cfg:';
|
||||
const cache = Object.create(null); // key -> string (only keys present in the DB)
|
||||
let dbAllFn = null;
|
||||
let dbRunFn = null;
|
||||
|
||||
function envOrDefault(key) {
|
||||
const e = process.env[key];
|
||||
return e && e.length ? e : DEFAULTS[key];
|
||||
}
|
||||
|
||||
// Synchronous read. Falls back to process.env (pre-migration / never saved)
|
||||
// then to the built-in default. After init() the DB value is cached and wins.
|
||||
function get(key) {
|
||||
const v = cache[key];
|
||||
if (v !== undefined) return v;
|
||||
return envOrDefault(key);
|
||||
}
|
||||
|
||||
// All keys with their effective values, keyed by name — used by the settings UI.
|
||||
function getAll() {
|
||||
const out = {};
|
||||
for (const key of Object.keys(DEFAULTS)) out[key] = get(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Ollama bundle (shared by lib/documents.js + lib/chat.js).
|
||||
function ollama() {
|
||||
return {
|
||||
host: (get('OLLAMA_HOST') || 'https://ollama.com').replace(/\/+$/, ''),
|
||||
model: get('OLLAMA_MODEL') || 'gpt-oss:120b',
|
||||
timeoutMs: Number(get('OLLAMA_TIMEOUT_MS')) || 300000,
|
||||
apiKey: get('OLLAMA_API_KEY') || '',
|
||||
};
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!dbAllFn) return;
|
||||
const rows = await dbAllFn('SELECT key, value FROM app_state WHERE key LIKE ?', [PREFIX + '%']);
|
||||
for (const r of rows) cache[r.key.slice(PREFIX.length)] = r.value;
|
||||
}
|
||||
|
||||
// Wire up DB helpers, load the cached rows, then one-time-migrate any env
|
||||
// value that isn't yet in the DB (so an existing .env install keeps its config).
|
||||
async function init({ dbAll, dbRun }) {
|
||||
dbAllFn = dbAll;
|
||||
dbRunFn = dbRun;
|
||||
await load();
|
||||
const toMigrate = [];
|
||||
for (const key of Object.keys(DEFAULTS)) {
|
||||
if (cache[key] === undefined) {
|
||||
const e = process.env[key];
|
||||
if (e && e.length) toMigrate.push([key, e]);
|
||||
}
|
||||
}
|
||||
if (toMigrate.length) {
|
||||
for (const [k, v] of toMigrate) {
|
||||
await dbRunFn(
|
||||
'INSERT INTO app_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
||||
[PREFIX + k, v]
|
||||
);
|
||||
cache[k] = v;
|
||||
}
|
||||
console.log(`Konfiguration aus .env in die Datenbank migriert (${toMigrate.length} Werte) — .env wird nicht mehr benötigt.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist every key (writes all rows, including empty strings, so a cleared
|
||||
// field is stored as empty and no longer falls back to env/default).
|
||||
async function saveAll(values) {
|
||||
if (!dbRunFn) throw new Error('Konfigurationsspeicher nicht initialisiert.');
|
||||
for (const key of Object.keys(DEFAULTS)) {
|
||||
const v = values && values[key] != null ? String(values[key]) : '';
|
||||
await dbRunFn(
|
||||
'INSERT INTO app_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
||||
[PREFIX + key, v]
|
||||
);
|
||||
cache[key] = v;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DEFAULTS, FIELDS, get, getAll, ollama, init, load, saveAll };
|
||||
+16
-18
@@ -11,6 +11,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const promptStore = require('./prompts');
|
||||
const designStore = require('./design');
|
||||
const config = require('./config');
|
||||
|
||||
// Embedded typefaces (all SIL Open Font License — see lib/fonts/OFL*.txt):
|
||||
// Lato — the classic layout's workhorse
|
||||
@@ -47,10 +48,8 @@ function makeDoc(t) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Ollama Cloud API (https://ollama.com). Override host/model via env if needed.
|
||||
const OLLAMA_HOST = (process.env.OLLAMA_HOST || 'https://ollama.com').replace(/\/+$/, '');
|
||||
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'gpt-oss:120b';
|
||||
const OLLAMA_TIMEOUT_MS = Number(process.env.OLLAMA_TIMEOUT_MS || 300000);
|
||||
// Ollama Cloud API (https://ollama.com). Host/model/timeout come from the DB
|
||||
// (editable via /einstellungen) and are read fresh per call via config.ollama().
|
||||
|
||||
// ===========================================================================
|
||||
// 1. LLM call — produce tailored TEXT for the template
|
||||
@@ -177,12 +176,11 @@ function buildOutputSchema(dokumente) {
|
||||
|
||||
async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen = '', zusatzAnlagen = [], prompts = null, dokumente = null }) {
|
||||
const doks = normalizeDokumente(dokumente);
|
||||
const apiKey = process.env.OLLAMA_API_KEY;
|
||||
const { host: ollamaHost, model: ollamaModel, timeoutMs: ollamaTimeoutMs, apiKey } = config.ollama();
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'OLLAMA_API_KEY ist nicht gesetzt. Bitte den API-Schlüssel als ' +
|
||||
'Umgebungsvariable (z. B. in einer .env-Datei) hinterlegen, damit ' +
|
||||
'Bewerbungsunterlagen generiert werden können.'
|
||||
'Kein Ollama-API-Schlüssel konfiguriert. Bitte unter „Einstellungen“ den ' +
|
||||
'API-Schlüssel eintragen, damit Bewerbungsunterlagen generiert werden können.'
|
||||
);
|
||||
}
|
||||
if (!basisDokumente || basisDokumente.length === 0) {
|
||||
@@ -428,15 +426,15 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
||||
`JSON-Objekt, ohne Markdown, ohne Code-Fences, ohne weiteren Text.`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT_MS);
|
||||
const timeout = setTimeout(() => controller.abort(), ollamaTimeoutMs);
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${OLLAMA_HOST}/api/chat`, {
|
||||
res = await fetch(`${ollamaHost}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({
|
||||
model: OLLAMA_MODEL,
|
||||
model: ollamaModel,
|
||||
stream: false,
|
||||
format: buildOutputSchema(doks),
|
||||
options: { temperature: 0.4 },
|
||||
@@ -449,7 +447,7 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
throw new Error(`Zeitüberschreitung bei der KI-Anfrage (> ${Math.round(OLLAMA_TIMEOUT_MS / 1000)}s).`);
|
||||
throw new Error(`Zeitüberschreitung bei der KI-Anfrage (> ${Math.round(ollamaTimeoutMs / 1000)}s).`);
|
||||
}
|
||||
throw new Error(`Verbindung zur Ollama-API fehlgeschlagen: ${err.message}`);
|
||||
} finally {
|
||||
@@ -1942,17 +1940,17 @@ async function generateApplicationDocuments({ job, basisDokumente, settings, zus
|
||||
|
||||
// Small shared Ollama JSON call (used by the reply drafter).
|
||||
async function ollamaChatJSON({ system, user, schema, temperature = 0.5 }) {
|
||||
const apiKey = process.env.OLLAMA_API_KEY;
|
||||
if (!apiKey) throw new Error('OLLAMA_API_KEY ist nicht gesetzt.');
|
||||
const { host: ollamaHost, model: ollamaModel, timeoutMs: ollamaTimeoutMs, apiKey } = config.ollama();
|
||||
if (!apiKey) throw new Error('Kein Ollama-API-Schlüssel konfiguriert (unter „Einstellungen“ eintragen).');
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT_MS);
|
||||
const timeout = setTimeout(() => controller.abort(), ollamaTimeoutMs);
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${OLLAMA_HOST}/api/chat`, {
|
||||
res = await fetch(`${ollamaHost}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({
|
||||
model: OLLAMA_MODEL,
|
||||
model: ollamaModel,
|
||||
stream: false,
|
||||
format: schema,
|
||||
options: { temperature },
|
||||
@@ -1964,7 +1962,7 @@ async function ollamaChatJSON({ system, user, schema, temperature = 0.5 }) {
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw new Error(`Zeitüberschreitung bei der KI-Anfrage (> ${Math.round(OLLAMA_TIMEOUT_MS / 1000)}s).`);
|
||||
if (err.name === 'AbortError') throw new Error(`Zeitüberschreitung bei der KI-Anfrage (> ${Math.round(ollamaTimeoutMs / 1000)}s).`);
|
||||
throw new Error(`Verbindung zur Ollama-API fehlgeschlagen: ${err.message}`);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
|
||||
+11
-8
@@ -11,18 +11,21 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { ImapFlow } = require('imapflow');
|
||||
const { simpleParser } = require('mailparser');
|
||||
const config = require('./config');
|
||||
|
||||
// Read fresh on every call so an edit on the /einstellungen page takes effect
|
||||
// immediately (values live in the DB now, not in process.env/.env).
|
||||
function cfg() {
|
||||
const smtpPort = Number(process.env.MAIL_SMTP_PORT || 587);
|
||||
const smtpPort = Number(config.get('MAIL_SMTP_PORT') || 587);
|
||||
return {
|
||||
host: process.env.MAIL_HOST || '',
|
||||
host: config.get('MAIL_HOST') || '',
|
||||
smtpPort,
|
||||
imapPort: Number(process.env.MAIL_IMAP_PORT || 993),
|
||||
user: process.env.MAIL_USER || '',
|
||||
pass: process.env.MAIL_PASSWORD || '',
|
||||
fromName: process.env.MAIL_FROM_NAME || '',
|
||||
fromAddr: process.env.MAIL_FROM || process.env.MAIL_USER || '',
|
||||
mailbox: process.env.MAIL_IMAP_MAILBOX || 'INBOX',
|
||||
imapPort: Number(config.get('MAIL_IMAP_PORT') || 993),
|
||||
user: config.get('MAIL_USER') || '',
|
||||
pass: config.get('MAIL_PASSWORD') || '',
|
||||
fromName: config.get('MAIL_FROM_NAME') || '',
|
||||
fromAddr: config.get('MAIL_FROM') || config.get('MAIL_USER') || '',
|
||||
mailbox: config.get('MAIL_IMAP_MAILBOX') || 'INBOX',
|
||||
// secure=true means implicit TLS (465); 587 uses STARTTLS (requireTLS).
|
||||
smtpSecure: smtpPort === 465,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user