test(portal): IMAP full-lifecycle activation E2E + dev probe helper
New realapi spec walks the entire portal activation loop over real network: invite via the admin endpoint → wait for the activation email to land in the IMAP mailbox → extract the token from the body link → activate the portal user via the public API → sign in with the new password. The match logic deliberately doesn't filter on the TO header — the combination of EMAIL_REDIRECT_TO rewriting and +addressing made TO matching brittle. Instead we discriminate by sender (noreply@…), subject keyword, and body link pattern, which is unique enough to find exactly the email this test triggered. Companion script scripts/dev-imap-probe.ts dumps the most recent ~10 messages with from/to/subject/date — useful for debugging when an IMAP match goes wrong. Skips when IMAP_HOST / IMAP_USER / IMAP_PASS are absent so the suite stays portable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
scripts/dev-imap-probe.ts
Normal file
66
scripts/dev-imap-probe.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Dev diagnostic: connect to IMAP and print the most recent ~10 messages,
|
||||
* showing TO/FROM/subject/date so we can see what the dev mailbox is
|
||||
* actually receiving.
|
||||
*
|
||||
* Run: pnpm tsx scripts/dev-imap-probe.ts
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { simpleParser } from 'mailparser';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const host = process.env.IMAP_HOST!;
|
||||
const port = Number(process.env.IMAP_PORT ?? 993);
|
||||
const user = process.env.IMAP_USER!;
|
||||
const pass = process.env.IMAP_PASS!;
|
||||
|
||||
if (!host || !user || !pass) {
|
||||
throw new Error('IMAP_HOST / IMAP_USER / IMAP_PASS not set');
|
||||
}
|
||||
|
||||
console.log(`Connecting to ${user}@${host}:${port}…`);
|
||||
const client = new ImapFlow({
|
||||
host,
|
||||
port,
|
||||
secure: port === 993,
|
||||
auth: { user, pass },
|
||||
logger: false,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
console.log('Connected. Inbox status:');
|
||||
const lock = await client.getMailboxLock('INBOX');
|
||||
try {
|
||||
const status = await client.status('INBOX', { messages: true, recent: true });
|
||||
console.log(' total:', status.messages, '| recent:', status.recent);
|
||||
|
||||
// Pull the last 10 by UID
|
||||
const since = new Date(Date.now() - 30 * 60 * 1000); // last 30 min
|
||||
const result = await client.search({ since });
|
||||
const uids = Array.isArray(result) ? result.slice(-10).reverse() : [];
|
||||
console.log(`Found ${uids.length} messages in last 30min:`);
|
||||
for (const uid of uids) {
|
||||
const msg = await client.fetchOne(String(uid), { source: true, envelope: true });
|
||||
if (!msg || !msg.source) continue;
|
||||
const parsed = await simpleParser(msg.source);
|
||||
const tos = (Array.isArray(parsed.to) ? parsed.to : parsed.to ? [parsed.to] : [])
|
||||
.flatMap((a) => a.value.map((v) => v.address ?? ''))
|
||||
.join(', ');
|
||||
console.log(
|
||||
` uid=${uid} date=${parsed.date?.toISOString()} from=${parsed.from?.text} to=${tos} subject=${parsed.subject}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
await client.logout();
|
||||
console.log('Done.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Probe failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user