Files

88 lines
3.7 KiB
JavaScript

/**
* Read a message back out of the recipient's mailbox, through the mail
* provider's webmail.
*
* Outbound mail configuration fails in ways the sender never notices: an
* implicit-TLS port announced as STARTTLS, AUTH offered on a listener that has
* none, or a relay the application can reach but the provider rejects. In every
* one of those the send call still returns success. The only proof that a
* setting is right is the message arriving, so these helpers exist for any role
* whose configuration decides how mail leaves it.
*
* `webmailSsoLogin(page, username, password, baseUrl)`
* Drives the provider's webmail login. The webmail redirects to Keycloak on
* its own (`oauth_login_redirect`), so this only has to click an SSO control
* when one is rendered, then fill the Keycloak form. A caller that already
* holds a Keycloak session never sees a form — the redirect answers straight
* back into the mailbox — so the helper waits for whichever of the two
* surfaces arrives instead of hanging on the form.
*
* `waitForEmailInMailbox(page, baseUrl, subject, timeout)`
* Polls INBOX and Junk for a message whose subject matches, and returns its
* row. Junk is not a fallback for a broken setup: a test domain publishes no
* mail-auth DNS, so a correctly delivered message is legitimately filed
* there. Folders are switched by `_mbox` URL, because clicking a folder link
* hangs when the row it points at is not actionable yet.
*/
const { performKeycloakLoginForm } = require("./keycloak");
const { gotoOnion } = require("./env");
const { resolveTimeout } = require("../../timeouts");
const MAIL_UI = "#messagelist, .compose, a[href*='_action=compose'], .toolbar";
const SSO_CONTROL = /sso|single sign.?on|login with|openid/i;
const MAILBOXES = ["INBOX", "Junk Mail"];
async function webmailSsoLogin(page, username, password, baseUrl) {
await gotoOnion(page, `${baseUrl}/`);
const ssoButton = page
.getByRole("button", { name: SSO_CONTROL })
.or(page.getByRole("link", { name: SSO_CONTROL }));
if (await ssoButton.first().isVisible().catch(() => false)) {
await ssoButton.first().click({ timeout: resolveTimeout(30_000) });
}
const mailUi = page.locator(MAIL_UI).first();
const usernameField = page
.getByRole("textbox", { name: /username|email/i })
.or(page.locator("input[name='username'], input#username"))
.first();
await Promise.any([
mailUi.waitFor({ state: "visible", timeout: resolveTimeout(60_000) }),
usernameField.waitFor({ state: "visible", timeout: resolveTimeout(60_000) }),
]);
if (await usernameField.isVisible().catch(() => false)) {
await performKeycloakLoginForm(page, username, password);
}
await mailUi.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
}
function messageRow(page, subject) {
return page
.locator("#messagelist tbody tr, table.messagelist tbody tr")
.filter({ hasText: subject })
.first();
}
async function waitForEmailInMailbox(page, baseUrl, subject, timeout = resolveTimeout(90_000)) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
for (const mailbox of MAILBOXES) {
await gotoOnion(page, `${baseUrl}/?_task=mail&_mbox=${encodeURIComponent(mailbox)}`, {
waitUntil: "domcontentloaded",
}).catch(() => {});
await page.waitForTimeout(resolveTimeout(2_000));
if (await messageRow(page, subject).isVisible().catch(() => false)) {
return messageRow(page, subject);
}
}
}
throw new Error(
`Timed out waiting for a message matching "${subject}" in ${MAILBOXES.join(" / ")}`
);
}
module.exports = { webmailSsoLogin, waitForEmailInMailbox };