About: name the webmail node and the mail server node #16
No files matched your search
@@ -44,6 +44,7 @@ import { imageProxyHandler } from "./imageproxy.js";
|
||||
import { SignInError, finish as finishSignIn, needsRefresh, oauthEnabled, refreshTokens, singleServer, start as startSignIn, type TokenSet } from "./oauth.js";
|
||||
import { icsProxyHandler } from "./icsproxy.js";
|
||||
import { staticHandler } from "./static.js";
|
||||
import { mailNode, webmailNode } from "./nodes.js";
|
||||
|
||||
type Env = { Variables: { session: LiveSession } };
|
||||
|
||||
@@ -609,6 +610,12 @@ export function createApp(basePath = config.basePath): Hono<Env> {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
/* ihasmail-inbuxa: which webmail node this is, and which mail node it talks to (nodes.ts). */
|
||||
api.get("/about/nodes", requireSession, async (c) => {
|
||||
const session = c.get("session");
|
||||
return c.json({ webmail: webmailNode(), mailServer: await mailNode(upstreamFor(session.username)) });
|
||||
});
|
||||
|
||||
api.get("/auth/sessions", requireSession, (c) => {
|
||||
const session = c.get("session");
|
||||
return c.json({ current: session.id, sessions: sessions.listForUser(session.account) });
|
||||
|
||||
@@ -296,6 +296,12 @@ export const config = {
|
||||
* such a tree, so the default is INBUXA's fork.
|
||||
*/
|
||||
sourceUrl: env("SOURCE_URL", "https://git.coffeylabs.org/inbuxa/ihasmail-inbuxa"),
|
||||
/**
|
||||
* ihasmail-inbuxa: this webmail's own name, shown in Settings > About next
|
||||
* to the mail node it talks to. Set per host by the deploy; empty means the
|
||||
* container's hostname. See nodes.ts.
|
||||
*/
|
||||
nodeName: env("NODE_NAME", ""),
|
||||
host: env("HOST", "0.0.0.0"),
|
||||
port: int("PORT", 8080),
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { test, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { clearNodeCache, mailNode } from "./nodes.js";
|
||||
|
||||
/**
|
||||
* About names the mail node by the PTR of the address the server's name
|
||||
* resolves to from the webmail -- the local node, where a host entry pins it.
|
||||
*/
|
||||
|
||||
beforeEach(() => clearNodeCache());
|
||||
|
||||
const lookup = (address: string) => async () => ({ address });
|
||||
|
||||
test("the node is named by its address's PTR, without the trailing dot", async () => {
|
||||
const n = await mailNode("https://mail.example.com", lookup("192.0.2.2"), async () => ["mx2.example.com."]);
|
||||
assert.deepEqual(n, { host: "mail.example.com", address: "192.0.2.2", name: "mx2.example.com" });
|
||||
});
|
||||
|
||||
test("an address without a PTR still shows the address", async () => {
|
||||
const n = await mailNode("https://mail.example.com", lookup("192.0.2.3"), async () => { throw new Error("ENOTFOUND"); });
|
||||
assert.deepEqual(n, { host: "mail.example.com", address: "192.0.2.3", name: null });
|
||||
});
|
||||
|
||||
test("a name that doesn't resolve says so instead of failing", async () => {
|
||||
let reversed = false;
|
||||
const n = await mailNode("https://mail.example.com", async () => { throw new Error("ENOTFOUND"); }, async () => { reversed = true; return []; });
|
||||
assert.deepEqual(n, { host: "mail.example.com", address: null, name: null });
|
||||
assert.equal(reversed, false);
|
||||
});
|
||||
|
||||
test("the answer is cached for a minute, then looked up again", async () => {
|
||||
let calls = 0;
|
||||
const count = async () => { calls++; return { address: "192.0.2.1" }; };
|
||||
const rev = async () => ["mail.example.com"];
|
||||
await mailNode("https://mail.example.com", count, rev, 0);
|
||||
await mailNode("https://mail.example.com", count, rev, 59_000);
|
||||
assert.equal(calls, 1);
|
||||
await mailNode("https://mail.example.com", count, rev, 61_000);
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* ihasmail-inbuxa: which webmail node answered, and which inbuxa node it talks
|
||||
* to -- shown in Settings > About, for troubleshooting a cluster.
|
||||
*
|
||||
* Not for upstream ihasmail: it only makes sense where one webmail runs per
|
||||
* host, each pinned to its own host's mail node.
|
||||
*
|
||||
* The webmail node is NODE_NAME, set per host by the deploy; without it, the
|
||||
* container's hostname, which is at least distinct. The mail node is worked
|
||||
* out from the address the server's name resolves to *here*, where the host
|
||||
* entry pins it to the local node, named by that address's PTR record (every
|
||||
* node has one: mail/mx2/mx3). The server offers its node name only to
|
||||
* administrators, so asking it would leave everyone else with nothing.
|
||||
*/
|
||||
import { lookup as dnsLookup, reverse as dnsReverse } from "node:dns/promises";
|
||||
import { hostname } from "node:os";
|
||||
import { config } from "./config.js";
|
||||
|
||||
export interface MailNode {
|
||||
/** The server name the webmail connects to, from its configured URL. */
|
||||
host: string;
|
||||
/** What that name resolves to from this container, or null if it doesn't. */
|
||||
address: string | null;
|
||||
/** The address's PTR name: the node's own name. Null without a PTR. */
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface Nodes {
|
||||
webmail: string;
|
||||
mailServer: MailNode;
|
||||
}
|
||||
|
||||
type Lookup = (host: string) => Promise<{ address: string }>;
|
||||
type Reverse = (address: string) => Promise<string[]>;
|
||||
|
||||
const CACHE_MS = 60_000;
|
||||
const cache = new Map<string, { node: MailNode; at: number }>();
|
||||
|
||||
export function webmailNode(): string {
|
||||
return config.nodeName || hostname();
|
||||
}
|
||||
|
||||
export async function mailNode(base: string, lookup: Lookup = dnsLookup, reverse: Reverse = dnsReverse, now = Date.now()): Promise<MailNode> {
|
||||
const host = new URL(base).hostname;
|
||||
const hit = cache.get(host);
|
||||
if (hit && now - hit.at < CACHE_MS) return hit.node;
|
||||
let address: string | null = null;
|
||||
let name: string | null = null;
|
||||
try {
|
||||
address = (await lookup(host)).address;
|
||||
} catch {
|
||||
/* unresolvable: say so rather than fail the page */
|
||||
}
|
||||
if (address) {
|
||||
try {
|
||||
name = (await reverse(address))[0]?.replace(/\.$/, "") ?? null;
|
||||
} catch {
|
||||
/* no PTR */
|
||||
}
|
||||
}
|
||||
const node = { host, address, name };
|
||||
cache.set(host, { node, at: now });
|
||||
return node;
|
||||
}
|
||||
|
||||
export function clearNodeCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
@@ -1740,6 +1740,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Zur Bestätigung {phrase} eingeben",
|
||||
"Turn off legacy protocols": "Ältere Mailprotokolle ausschalten",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Ältere Mailprotokolle sind für Ihre Organisation ausgeschaltet. Nur {app} und JMAP-Apps können sich anmelden.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Webmail-Knoten",
|
||||
"Mail server node": "Mailserver-Knoten",
|
||||
"{host} does not resolve": "{host} lässt sich nicht auflösen",
|
||||
"unavailable": "nicht verfügbar",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Einschätzung des Sprachmodells",
|
||||
"One of several signals the spam filter weighed": "Eines von mehreren Signalen, die der Spamfilter berücksichtigt hat",
|
||||
|
||||
@@ -1713,6 +1713,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Para confirmar, escriba {phrase}",
|
||||
"Turn off legacy protocols": "Desactivar los protocolos de correo heredados",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Los protocolos de correo heredados están desactivados para su organización. Solo {app} y las aplicaciones JMAP pueden iniciar sesión.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Nodo del webmail",
|
||||
"Mail server node": "Nodo del servidor de correo",
|
||||
"{host} does not resolve": "{host} no se resuelve",
|
||||
"unavailable": "no disponible",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Opinión del modelo de lenguaje",
|
||||
"One of several signals the spam filter weighed": "Una de varias señales que el filtro de spam ha tenido en cuenta",
|
||||
|
||||
@@ -1718,6 +1718,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Pour confirmer, saisissez {phrase}",
|
||||
"Turn off legacy protocols": "Désactiver les protocoles de messagerie historiques",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Les protocoles de messagerie historiques sont désactivés pour votre organisation. Seuls {app} et les applications JMAP peuvent se connecter.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Nœud du webmail",
|
||||
"Mail server node": "Nœud du serveur de messagerie",
|
||||
"{host} does not resolve": "{host} ne se résout pas",
|
||||
"unavailable": "indisponible",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Avis du modèle de langage",
|
||||
"One of several signals the spam filter weighed": "Un signal parmi d'autres pris en compte par le filtre antispam",
|
||||
|
||||
@@ -1721,6 +1721,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "確認のため {phrase} と入力してください",
|
||||
"Turn off legacy protocols": "従来のメールプロトコルをオフにする",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "組織では従来のメールプロトコルがオフになっています。サインインできるのは {app} と JMAP アプリのみです。",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Webメールのノード",
|
||||
"Mail server node": "メールサーバーのノード",
|
||||
"{host} does not resolve": "{host} の名前解決ができません",
|
||||
"unavailable": "利用できません",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "言語モデルの見解",
|
||||
"One of several signals the spam filter weighed": "迷惑メールフィルターが考慮した複数の判断材料のひとつ",
|
||||
|
||||
@@ -1710,6 +1710,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Typ ter bevestiging {phrase}",
|
||||
"Turn off legacy protocols": "Verouderde mailprotocollen uitschakelen",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Verouderde mailprotocollen zijn uitgeschakeld voor uw organisatie. Alleen {app} en JMAP-apps kunnen inloggen.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Webmailnode",
|
||||
"Mail server node": "Mailservernode",
|
||||
"{host} does not resolve": "{host} wordt niet omgezet",
|
||||
"unavailable": "niet beschikbaar",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Oordeel van het taalmodel",
|
||||
"One of several signals the spam filter weighed": "Een van meerdere signalen die het spamfilter heeft meegewogen",
|
||||
|
||||
@@ -1716,6 +1716,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Para confirmar, digite {phrase}",
|
||||
"Turn off legacy protocols": "Desativar os protocolos de e-mail legados",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Os protocolos de e-mail legados estão desativados para sua organização. Só {app} e aplicativos JMAP podem entrar.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Nó do webmail",
|
||||
"Mail server node": "Nó do servidor de e-mail",
|
||||
"{host} does not resolve": "{host} não resolve",
|
||||
"unavailable": "indisponível",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Opinião do modelo de linguagem",
|
||||
"One of several signals the spam filter weighed": "Um dos vários sinais considerados pelo filtro de spam",
|
||||
|
||||
@@ -1715,6 +1715,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Для подтверждения введите {phrase}",
|
||||
"Turn off legacy protocols": "Отключить устаревшие почтовые протоколы",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Устаревшие почтовые протоколы отключены для вашей организации. Входить могут только {app} и приложения JMAP.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Узел веб-почты",
|
||||
"Mail server node": "Узел почтового сервера",
|
||||
"{host} does not resolve": "{host} не разрешается",
|
||||
"unavailable": "недоступно",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Мнение языковой модели",
|
||||
"One of several signals the spam filter weighed": "Один из нескольких признаков, которые учёл спам-фильтр",
|
||||
|
||||
@@ -1709,6 +1709,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "Для підтвердження введіть {phrase}",
|
||||
"Turn off legacy protocols": "Вимкнути застарілі поштові протоколи",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Застарілі поштові протоколи вимкнено для вашої організації. Входити можуть лише {app} і програми JMAP.",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "Вузол вебпошти",
|
||||
"Mail server node": "Вузол поштового сервера",
|
||||
"{host} does not resolve": "{host} не розпізнається",
|
||||
"unavailable": "недоступно",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "Думка мовної моделі",
|
||||
"One of several signals the spam filter weighed": "Одна з кількох ознак, які врахував спам-фільтр",
|
||||
|
||||
@@ -1720,6 +1720,11 @@ export const catalog: Catalog = {
|
||||
"To confirm, type {phrase}": "请输入 {phrase} 以确认",
|
||||
"Turn off legacy protocols": "关闭传统邮件协议",
|
||||
"Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "您的组织已关闭传统邮件协议。只有 {app} 和 JMAP 应用可以登录。",
|
||||
// ── About: webmail and mail server nodes (inbuxa) ──────────────
|
||||
"Webmail node": "网页邮箱节点",
|
||||
"Mail server node": "邮件服务器节点",
|
||||
"{host} does not resolve": "无法解析 {host}",
|
||||
"unavailable": "不可用",
|
||||
// ── Spam filter: the language model's opinion (inbuxa) ──────────
|
||||
"Language model's opinion": "语言模型的判断",
|
||||
"One of several signals the spam filter weighed": "垃圾邮件过滤考虑的多个信号之一",
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "@/store/session";
|
||||
import { useAppName } from "@/lib/brand";
|
||||
import { client } from "@/jmap/client";
|
||||
import { apiFetch, client } from "@/jmap/client";
|
||||
import { APP_VERSION } from "@/lib/version";
|
||||
import { DEFAULT_SOURCE_URL } from "@/lib/source";
|
||||
import { withBase } from "@/lib/basePath";
|
||||
import { t, tNode } from "@/lib/i18n";
|
||||
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
|
||||
|
||||
/** ihasmail-inbuxa: which webmail node answered and which mail node it talks to (server/src/nodes.ts). */
|
||||
interface Nodes {
|
||||
webmail: string;
|
||||
mailServer: { host: string; address: string | null; name: string | null };
|
||||
}
|
||||
|
||||
function mailNodeLabel(m: Nodes["mailServer"]): string {
|
||||
if (!m.address) return t("{host} does not resolve", { host: m.host });
|
||||
return m.name ? `${m.name} (${m.address})` : m.address;
|
||||
}
|
||||
|
||||
export function AboutSettings() {
|
||||
const appName = useAppName();
|
||||
const session = useSession((s) => s.session);
|
||||
// Fetched on every visit, not cached with the session: when troubleshooting,
|
||||
// the question is which nodes are in use now.
|
||||
const [nodes, setNodes] = useState<Nodes | null | "error">(null);
|
||||
useEffect(() => {
|
||||
apiFetch<Nodes>("/api/about/nodes").then(setNodes).catch(() => setNodes("error"));
|
||||
}, []);
|
||||
const nodeCell = (v: (n: Nodes) => string) =>
|
||||
nodes === null ? t("Loading…") : nodes === "error" ? t("unavailable") : <span className="mono notranslate" translate="no">{v(nodes)}</span>;
|
||||
const caps = Object.keys(session?.capabilities ?? {});
|
||||
// A deployment running modified code should offer its own source, not ours.
|
||||
const sourceUrl = session?.ihasmail?.sourceUrl ?? DEFAULT_SOURCE_URL;
|
||||
@@ -36,6 +56,8 @@ export function AboutSettings() {
|
||||
<tbody>
|
||||
<tr><td>{t("Signed in as")}</td><td>{session?.username}</td></tr>
|
||||
<tr><td>{t("Mail server")}</td><td className="notranslate" translate="no">inbuxa</td></tr>
|
||||
<tr><td>{t("Webmail node")}</td><td>{nodeCell((n) => n.webmail)}</td></tr>
|
||||
<tr><td>{t("Mail server node")}</td><td>{nodeCell((n) => mailNodeLabel(n.mailServer))}</td></tr>
|
||||
<tr><td>{t("Accounts")}</td><td>{Object.values(session?.accounts ?? {}).map((a) => a.name).join(", ")}</td></tr>
|
||||
<tr><td>{t("Max upload")}</td><td>{t("{size} MB", { size: Math.round(client.maxSizeUpload / 1048576) })}</td></tr>
|
||||
<tr><td>{t("Image privacy proxy")}</td><td>{session?.ihasmail?.imageProxy ? t("enabled") : t("disabled")}</td></tr>
|
||||
|
||||
Reference in new issue
Block a user