import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js'; let testServer: ITestServer; tap.test('setup test SMTP server', async () => { testServer = await startTestServer({ port: 2562, tlsEnabled: false, authRequired: true }); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CSEC-02: Check OAuth2 support', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Check EHLO response for OAuth support const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com'); console.log('Checking OAuth2 support in EHLO response...'); const supportsXOAuth2 = ehloResponse.includes('XOAUTH2'); const supportsOAuthBearer = ehloResponse.includes('OAUTHBEARER'); console.log(`XOAUTH2 supported: ${supportsXOAuth2}`); console.log(`OAUTHBEARER supported: ${supportsOAuthBearer}`); if (!supportsXOAuth2 && !supportsOAuthBearer) { console.log('Server does not advertise OAuth2 support'); } await smtpClient.close(); }); tap.test('CSEC-02: XOAUTH2 authentication flow', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Create XOAUTH2 string // Format: base64("user=" + user + "^Aauth=Bearer " + token + "^A^A") const user = 'user@example.com'; const accessToken = 'mock-oauth2-access-token'; const authString = `user=${user}\x01auth=Bearer ${accessToken}\x01\x01`; const base64Auth = Buffer.from(authString).toString('base64'); console.log('\nAttempting XOAUTH2 authentication...'); console.log(`User: ${user}`); console.log(`Token: ${accessToken.substring(0, 10)}...`); try { const authResponse = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`); if (authResponse.startsWith('235')) { console.log('XOAUTH2 authentication successful'); expect(authResponse).toInclude('235'); } else if (authResponse.startsWith('334')) { // Server wants more data or error response console.log('Server response:', authResponse); // Send empty response to get error details const errorResponse = await smtpClient.sendCommand(''); console.log('Error details:', errorResponse); } else { console.log('Authentication failed:', authResponse); } } catch (error) { console.log('XOAUTH2 not supported or failed:', error.message); } await smtpClient.close(); }); tap.test('CSEC-02: OAUTHBEARER authentication flow', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Create OAUTHBEARER string (RFC 7628) // Format: n,a=user@example.com,^Ahost=server.example.com^Aport=587^Aauth=Bearer token^A^A const user = 'user@example.com'; const accessToken = 'mock-oauthbearer-access-token'; const authString = `n,a=${user},\x01host=${testServer.hostname}\x01port=${testServer.port}\x01auth=Bearer ${accessToken}\x01\x01`; const base64Auth = Buffer.from(authString).toString('base64'); console.log('\nAttempting OAUTHBEARER authentication...'); console.log(`User: ${user}`); console.log(`Host: ${testServer.hostname}`); console.log(`Port: ${testServer.port}`); try { const authResponse = await smtpClient.sendCommand(`AUTH OAUTHBEARER ${base64Auth}`); if (authResponse.startsWith('235')) { console.log('OAUTHBEARER authentication successful'); expect(authResponse).toInclude('235'); } else if (authResponse.startsWith('334')) { // Server wants more data or error response console.log('Server challenge:', authResponse); // Decode challenge if present const challenge = authResponse.substring(4).trim(); if (challenge) { const decodedChallenge = Buffer.from(challenge, 'base64').toString(); console.log('Decoded challenge:', decodedChallenge); } // Send empty response to cancel await smtpClient.sendCommand('*'); } else { console.log('Authentication failed:', authResponse); } } catch (error) { console.log('OAUTHBEARER not supported or failed:', error.message); } await smtpClient.close(); }); tap.test('CSEC-02: OAuth2 with client configuration', async () => { // Test client with OAuth2 configuration const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: { type: 'oauth2', user: 'oauth.user@example.com', clientId: 'client-id-12345', clientSecret: 'client-secret-67890', accessToken: 'access-token-abcdef', refreshToken: 'refresh-token-ghijkl', expires: Date.now() + 3600000 // 1 hour from now }, connectionTimeout: 5000, debug: true }); try { await smtpClient.connect(); // Check if client handles OAuth2 auth automatically const authenticated = await smtpClient.isAuthenticated(); console.log('OAuth2 auto-authentication:', authenticated ? 'Success' : 'Failed'); if (authenticated) { // Try to send a test email const result = await smtpClient.verify(); console.log('Connection verified:', result); } } catch (error) { console.log('OAuth2 configuration test:', error.message); // Expected if server doesn't support OAuth2 } await smtpClient.close(); }); tap.test('CSEC-02: OAuth2 token refresh simulation', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Simulate expired token scenario const user = 'user@example.com'; const expiredToken = 'expired-access-token'; const authString = `user=${user}\x01auth=Bearer ${expiredToken}\x01\x01`; const base64Auth = Buffer.from(authString).toString('base64'); console.log('\nSimulating expired token scenario...'); try { const authResponse = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`); if (authResponse.startsWith('334')) { // Server returns error, decode it const errorBase64 = authResponse.substring(4).trim(); if (errorBase64) { const errorJson = Buffer.from(errorBase64, 'base64').toString(); console.log('OAuth2 error response:', errorJson); try { const error = JSON.parse(errorJson); if (error.status === '401') { console.log('Token expired or invalid - would trigger refresh'); // Simulate token refresh const newToken = 'refreshed-access-token'; const newAuthString = `user=${user}\x01auth=Bearer ${newToken}\x01\x01`; const newBase64Auth = Buffer.from(newAuthString).toString('base64'); // Cancel current auth await smtpClient.sendCommand('*'); // Try again with new token console.log('Retrying with refreshed token...'); const retryResponse = await smtpClient.sendCommand(`AUTH XOAUTH2 ${newBase64Auth}`); console.log('Retry response:', retryResponse); } } catch (e) { console.log('Error response not JSON:', errorJson); } } } } catch (error) { console.log('Token refresh simulation error:', error.message); } await smtpClient.close(); }); tap.test('CSEC-02: OAuth2 scope validation', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test different OAuth2 scopes const testScopes = [ { scope: 'https://mail.google.com/', desc: 'Gmail full access' }, { scope: 'https://outlook.office.com/SMTP.Send', desc: 'Outlook send-only' }, { scope: 'email', desc: 'Generic email scope' } ]; for (const test of testScopes) { console.log(`\nTesting OAuth2 with scope: ${test.desc}`); const user = 'user@example.com'; const token = `token-with-scope-${test.scope.replace(/[^a-z]/gi, '')}`; // Include scope in auth string (non-standard, for testing) const authString = `user=${user}\x01auth=Bearer ${token}\x01scope=${test.scope}\x01\x01`; const base64Auth = Buffer.from(authString).toString('base64'); try { const response = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`); console.log(`Response for ${test.desc}: ${response.substring(0, 50)}...`); if (response.startsWith('334') || response.startsWith('535')) { // Cancel auth attempt await smtpClient.sendCommand('*'); } } catch (error) { console.log(`Error for ${test.desc}: ${error.message}`); } } await smtpClient.close(); }); tap.test('CSEC-02: OAuth2 provider-specific formats', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test provider-specific OAuth2 formats const providers = [ { name: 'Google', format: (user: string, token: string) => `user=${user}\x01auth=Bearer ${token}\x01\x01` }, { name: 'Microsoft', format: (user: string, token: string) => `user=${user}\x01auth=Bearer ${token}\x01\x01` }, { name: 'Yahoo', format: (user: string, token: string) => `user=${user}\x01auth=Bearer ${token}\x01\x01` } ]; for (const provider of providers) { console.log(`\nTesting ${provider.name} OAuth2 format...`); const user = `test@${provider.name.toLowerCase()}.com`; const token = `${provider.name.toLowerCase()}-oauth-token`; const authString = provider.format(user, token); const base64Auth = Buffer.from(authString).toString('base64'); try { const response = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`); console.log(`${provider.name} response: ${response.substring(0, 30)}...`); if (!response.startsWith('235')) { // Cancel if not successful await smtpClient.sendCommand('*'); } } catch (error) { console.log(`${provider.name} error: ${error.message}`); } } await smtpClient.close(); }); tap.test('CSEC-02: OAuth2 security considerations', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nOAuth2 Security Considerations:'); // Check if connection is encrypted const connectionInfo = smtpClient.getConnectionInfo(); console.log(`Connection encrypted: ${connectionInfo?.secure || false}`); if (!connectionInfo?.secure) { console.log('WARNING: OAuth2 over unencrypted connection is insecure!'); } // Check STARTTLS availability const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com'); const supportsStartTLS = ehloResponse.includes('STARTTLS'); if (supportsStartTLS && !connectionInfo?.secure) { console.log('STARTTLS available - upgrading connection...'); try { const starttlsResponse = await smtpClient.sendCommand('STARTTLS'); if (starttlsResponse.startsWith('220')) { console.log('Connection upgraded to TLS'); // In real implementation, TLS handshake would happen here } } catch (error) { console.log('STARTTLS failed:', error.message); } } // Test token exposure in logs const sensitiveToken = 'super-secret-oauth-token-12345'; const safeLogToken = sensitiveToken.substring(0, 10) + '...'; console.log(`Token handling - shown as: ${safeLogToken}`); await smtpClient.close(); }); tap.test('CSEC-02: OAuth2 error handling', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test various OAuth2 error scenarios const errorScenarios = [ { name: 'Invalid token format', authString: 'invalid-base64-!@#$' }, { name: 'Empty token', authString: Buffer.from('user=test@example.com\x01auth=Bearer \x01\x01').toString('base64') }, { name: 'Missing user', authString: Buffer.from('auth=Bearer token123\x01\x01').toString('base64') }, { name: 'Malformed structure', authString: Buffer.from('user=test@example.com auth=Bearer token').toString('base64') } ]; for (const scenario of errorScenarios) { console.log(`\nTesting: ${scenario.name}`); try { const response = await smtpClient.sendCommand(`AUTH XOAUTH2 ${scenario.authString}`); console.log(`Response: ${response}`); if (response.startsWith('334') || response.startsWith('501') || response.startsWith('535')) { // Expected error responses await smtpClient.sendCommand('*'); // Cancel } } catch (error) { console.log(`Error (expected): ${error.message}`); } } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await stopTestServer(testServer); } }); export default tap.start();