feat(mailer-smtp): add SCRAM-SHA-256 auth, Ed25519 DKIM, opportunistic TLS, SNI cert selection, pipelining and delivery/bridge improvements
This commit is contained in:
@@ -285,6 +285,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
auth?: { user: string; pass: string };
|
||||
dkimDomain?: string;
|
||||
dkimSelector?: string;
|
||||
tlsOpportunistic?: boolean;
|
||||
}): Promise<ISmtpSendResult> {
|
||||
// Build DKIM config if domain has keys
|
||||
let dkim: { domain: string; selector: string; privateKey: string } | undefined;
|
||||
@@ -321,6 +322,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
socketTimeoutSecs: Math.floor((this.options.outbound?.socketTimeout || 120000) / 1000),
|
||||
poolKey: `${host}:${port}`,
|
||||
maxPoolConnections: this.options.outbound?.maxConnections || 10,
|
||||
tlsOpportunistic: options?.tlsOpportunistic ?? (port === 25),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,6 +418,22 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.rustBridge.onScramCredentialRequest(async (data) => {
|
||||
try {
|
||||
await this.handleScramCredentialRequest(data);
|
||||
} catch (err) {
|
||||
logger.log('error', `Error handling SCRAM credential request: ${(err as Error).message}`);
|
||||
try {
|
||||
await this.rustBridge.sendScramCredentialResult({
|
||||
correlationId: data.correlationId,
|
||||
found: false,
|
||||
});
|
||||
} catch (sendErr) {
|
||||
logger.log('warn', `Could not send SCRAM credential rejection: ${(sendErr as Error).message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async startSmtpServer(): Promise<void> {
|
||||
@@ -622,6 +640,53 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a SCRAM credential request from the Rust SMTP server.
|
||||
* Computes SCRAM-SHA-256 credentials from the stored password for the given user.
|
||||
*/
|
||||
private async handleScramCredentialRequest(data: { correlationId: string; username: string; remoteAddr: string }): Promise<void> {
|
||||
const { correlationId, username, remoteAddr } = data;
|
||||
const crypto = await import('crypto');
|
||||
|
||||
logger.log('info', `SCRAM credential request for user=${username} from=${remoteAddr}`);
|
||||
|
||||
const users = this.options.auth?.users || [];
|
||||
const matched = users.find(u => u.username === username);
|
||||
|
||||
if (!matched) {
|
||||
await this.rustBridge.sendScramCredentialResult({
|
||||
correlationId,
|
||||
found: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute SCRAM-SHA-256 credentials from plaintext password
|
||||
const salt = crypto.randomBytes(16);
|
||||
const iterations = 4096;
|
||||
|
||||
// SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
|
||||
const saltedPassword = crypto.pbkdf2Sync(matched.password, salt, iterations, 32, 'sha256');
|
||||
|
||||
// ClientKey = HMAC-SHA256(SaltedPassword, "Client Key")
|
||||
const clientKey = crypto.createHmac('sha256', saltedPassword).update('Client Key').digest();
|
||||
|
||||
// StoredKey = SHA256(ClientKey)
|
||||
const storedKey = crypto.createHash('sha256').update(clientKey).digest();
|
||||
|
||||
// ServerKey = HMAC-SHA256(SaltedPassword, "Server Key")
|
||||
const serverKey = crypto.createHmac('sha256', saltedPassword).update('Server Key').digest();
|
||||
|
||||
await this.rustBridge.sendScramCredentialResult({
|
||||
correlationId,
|
||||
found: true,
|
||||
salt: salt.toString('base64'),
|
||||
iterations,
|
||||
storedKey: storedKey.toString('base64'),
|
||||
serverKey: serverKey.toString('base64'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results
|
||||
* or falling back to IPC call if no pre-computed results are available.
|
||||
|
||||
Reference in New Issue
Block a user