fix(mail): align queue, outbound hostname, and DKIM selector behavior across the mail server APIs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import { type IStorageManagerLike, hasStorageManagerMethods } from '../interfaces.storage.js';
|
||||
|
||||
import { Email } from '../core/classes.email.js';
|
||||
// MtaService reference removed
|
||||
@@ -24,13 +25,47 @@ export interface IDkimKeyMetadata {
|
||||
|
||||
export class DKIMCreator {
|
||||
private keysDir: string;
|
||||
private storageManager?: any; // StorageManager instance
|
||||
private storageManager?: IStorageManagerLike;
|
||||
|
||||
constructor(keysDir = paths.keysDir, storageManager?: any) {
|
||||
constructor(keysDir = paths.keysDir, storageManager?: IStorageManagerLike) {
|
||||
this.keysDir = keysDir;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
private async writeKeyPairToFilesystem(
|
||||
privateKeyPath: string,
|
||||
publicKeyPath: string,
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
): Promise<void> {
|
||||
await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
|
||||
}
|
||||
|
||||
private async storeLegacyKeysToStorage(domain: string, privateKey: string, publicKey: string): Promise<void> {
|
||||
if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([
|
||||
this.storageManager.set(`/email/dkim/${domain}/private.key`, privateKey),
|
||||
this.storageManager.set(`/email/dkim/${domain}/public.key`, publicKey),
|
||||
]);
|
||||
}
|
||||
|
||||
private async storeSelectorKeysToStorage(
|
||||
domain: string,
|
||||
selector: string,
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
): Promise<void> {
|
||||
if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([
|
||||
this.storageManager.set(`/email/dkim/${domain}/${selector}/private.key`, privateKey),
|
||||
this.storageManager.set(`/email/dkim/${domain}/${selector}/public.key`, publicKey),
|
||||
]);
|
||||
}
|
||||
|
||||
public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> {
|
||||
return {
|
||||
privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`),
|
||||
@@ -51,6 +86,20 @@ export class DKIMCreator {
|
||||
}
|
||||
}
|
||||
|
||||
public async handleDKIMKeysForSelector(domainArg: string, selector: string = 'default', keySize: number = 2048): Promise<void> {
|
||||
if (selector === 'default') {
|
||||
await this.handleDKIMKeysForDomain(domainArg);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.readDKIMKeysForSelector(domainArg, selector);
|
||||
} catch {
|
||||
console.log(`No DKIM keys found for ${domainArg}/${selector}. Generating...`);
|
||||
await this.createAndStoreDKIMKeysForSelector(domainArg, selector, keySize);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleDKIMKeysForEmail(email: Email): Promise<void> {
|
||||
const domain = email.from.split('@')[1];
|
||||
await this.handleDKIMKeysForDomain(domain);
|
||||
@@ -59,7 +108,7 @@ export class DKIMCreator {
|
||||
// Read DKIM keys - always use storage manager, migrate from filesystem if needed
|
||||
public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> {
|
||||
// Try to read from storage manager first
|
||||
if (this.storageManager) {
|
||||
if (hasStorageManagerMethods(this.storageManager, ['get', 'set'])) {
|
||||
try {
|
||||
const [privateKey, publicKey] = await Promise.all([
|
||||
this.storageManager.get(`/email/dkim/${domainArg}/private.key`),
|
||||
@@ -87,10 +136,7 @@ export class DKIMCreator {
|
||||
|
||||
// Migrate to storage manager
|
||||
console.log(`Migrating DKIM keys for ${domainArg} from filesystem to StorageManager`);
|
||||
await Promise.all([
|
||||
this.storageManager.set(`/email/dkim/${domainArg}/private.key`, privateKey),
|
||||
this.storageManager.set(`/email/dkim/${domainArg}/public.key`, publicKey)
|
||||
]);
|
||||
await this.storeLegacyKeysToStorage(domainArg, privateKey, publicKey);
|
||||
|
||||
return { privateKey, publicKey };
|
||||
} catch (error) {
|
||||
@@ -116,9 +162,9 @@ export class DKIMCreator {
|
||||
}
|
||||
|
||||
// Create an RSA DKIM key pair - changed to public for API access
|
||||
public async createDKIMKeys(): Promise<{ privateKey: string; publicKey: string }> {
|
||||
public async createDKIMKeys(keySize: number = 2048): Promise<{ privateKey: string; publicKey: string }> {
|
||||
const { privateKey, publicKey } = await generateKeyPair('rsa', {
|
||||
modulusLength: 2048,
|
||||
modulusLength: keySize,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||
});
|
||||
@@ -136,75 +182,58 @@ export class DKIMCreator {
|
||||
return { privateKey, publicKey };
|
||||
}
|
||||
|
||||
// Store a DKIM key pair - uses storage manager if available, else disk
|
||||
public async storeDKIMKeys(
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
privateKeyPath: string,
|
||||
publicKeyPath: string
|
||||
): Promise<void> {
|
||||
// Store in storage manager if available
|
||||
if (this.storageManager) {
|
||||
// Extract domain from path (e.g., /path/to/keys/example.com-private.pem -> example.com)
|
||||
const match = privateKeyPath.match(/\/([^\/]+)-private\.pem$/);
|
||||
if (match) {
|
||||
const domain = match[1];
|
||||
await Promise.all([
|
||||
this.storageManager.set(`/email/dkim/${domain}/private.key`, privateKey),
|
||||
this.storageManager.set(`/email/dkim/${domain}/public.key`, publicKey)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Also store to filesystem for backward compatibility
|
||||
await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
|
||||
}
|
||||
|
||||
// Create a DKIM key pair and store it to disk - changed to public for API access
|
||||
public async createAndStoreDKIMKeys(domain: string): Promise<void> {
|
||||
const { privateKey, publicKey } = await this.createDKIMKeys();
|
||||
public async createAndStoreDKIMKeys(domain: string, keySize: number = 2048): Promise<void> {
|
||||
const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
|
||||
const keyPaths = await this.getKeyPathsForDomain(domain);
|
||||
await this.storeDKIMKeys(
|
||||
privateKey,
|
||||
publicKey,
|
||||
keyPaths.privateKeyPath,
|
||||
keyPaths.publicKeyPath
|
||||
);
|
||||
await this.storeLegacyKeysToStorage(domain, privateKey, publicKey);
|
||||
await this.writeKeyPairToFilesystem(keyPaths.privateKeyPath, keyPaths.publicKeyPath, privateKey, publicKey);
|
||||
await this.saveKeyMetadata({
|
||||
domain,
|
||||
selector: 'default',
|
||||
createdAt: Date.now(),
|
||||
keySize,
|
||||
});
|
||||
console.log(`DKIM keys for ${domain} created and stored.`);
|
||||
}
|
||||
|
||||
public async createAndStoreDKIMKeysForSelector(
|
||||
domain: string,
|
||||
selector: string,
|
||||
keySize: number = 2048,
|
||||
): Promise<void> {
|
||||
if (selector === 'default') {
|
||||
await this.createAndStoreDKIMKeys(domain, keySize);
|
||||
return;
|
||||
}
|
||||
|
||||
const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
|
||||
const keyPaths = await this.getKeyPathsForSelector(domain, selector);
|
||||
await this.storeSelectorKeysToStorage(domain, selector, privateKey, publicKey);
|
||||
await this.writeKeyPairToFilesystem(keyPaths.privateKeyPath, keyPaths.publicKeyPath, privateKey, publicKey);
|
||||
await this.saveKeyMetadata({
|
||||
domain,
|
||||
selector,
|
||||
createdAt: Date.now(),
|
||||
keySize,
|
||||
});
|
||||
console.log(`DKIM keys for ${domain}/${selector} created and stored.`);
|
||||
}
|
||||
|
||||
// Changed to public for API access
|
||||
public async getDNSRecordForDomain(domainArg: string): Promise<plugins.tsclass.network.IDnsRecord> {
|
||||
await this.handleDKIMKeysForDomain(domainArg);
|
||||
const keys = await this.readDKIMKeys(domainArg);
|
||||
|
||||
// Remove the PEM header and footer and newlines
|
||||
const pemHeader = '-----BEGIN PUBLIC KEY-----';
|
||||
const pemFooter = '-----END PUBLIC KEY-----';
|
||||
const keyContents = keys.publicKey
|
||||
.replace(pemHeader, '')
|
||||
.replace(pemFooter, '')
|
||||
.replace(/\n/g, '');
|
||||
|
||||
// Detect key type from PEM header
|
||||
const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200 ? 'ed25519' : 'rsa';
|
||||
|
||||
// Now generate the DKIM DNS TXT record
|
||||
const dnsRecordValue = `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`;
|
||||
|
||||
return {
|
||||
name: `mta._domainkey.${domainArg}`,
|
||||
type: 'TXT',
|
||||
dnsSecEnabled: null,
|
||||
value: dnsRecordValue,
|
||||
};
|
||||
public async getDNSRecordForDomain(
|
||||
domainArg: string,
|
||||
selector: string = 'default',
|
||||
): Promise<plugins.tsclass.network.IDnsRecord> {
|
||||
await this.handleDKIMKeysForSelector(domainArg, selector);
|
||||
return this.getDNSRecordForSelector(domainArg, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DKIM key metadata for a domain
|
||||
*/
|
||||
private async getKeyMetadata(domain: string, selector: string = 'default'): Promise<IDkimKeyMetadata | null> {
|
||||
if (!this.storageManager) {
|
||||
if (!hasStorageManagerMethods(this.storageManager, ['get'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -222,7 +251,7 @@ export class DKIMCreator {
|
||||
* Save DKIM key metadata
|
||||
*/
|
||||
private async saveKeyMetadata(metadata: IDkimKeyMetadata): Promise<void> {
|
||||
if (!this.storageManager) {
|
||||
if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -259,30 +288,16 @@ export class DKIMCreator {
|
||||
const newSelector = `key${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
// Create new keys with custom key size
|
||||
const { privateKey, publicKey } = await generateKeyPair('rsa', {
|
||||
modulusLength: keySize,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||
});
|
||||
const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
|
||||
|
||||
// Store new keys with new selector
|
||||
const newKeyPaths = await this.getKeyPathsForSelector(domain, newSelector);
|
||||
|
||||
// Store in storage manager if available
|
||||
if (this.storageManager) {
|
||||
await Promise.all([
|
||||
this.storageManager.set(`/email/dkim/${domain}/${newSelector}/private.key`, privateKey),
|
||||
this.storageManager.set(`/email/dkim/${domain}/${newSelector}/public.key`, publicKey)
|
||||
]);
|
||||
}
|
||||
await this.storeSelectorKeysToStorage(domain, newSelector, privateKey, publicKey);
|
||||
|
||||
// Also store to filesystem
|
||||
await this.storeDKIMKeys(
|
||||
privateKey,
|
||||
publicKey,
|
||||
newKeyPaths.privateKeyPath,
|
||||
newKeyPaths.publicKeyPath
|
||||
);
|
||||
await this.writeKeyPairToFilesystem(newKeyPaths.privateKeyPath, newKeyPaths.publicKeyPath, privateKey, publicKey);
|
||||
|
||||
// Save metadata for new keys
|
||||
const metadata: IDkimKeyMetadata = {
|
||||
@@ -320,7 +335,7 @@ export class DKIMCreator {
|
||||
*/
|
||||
public async readDKIMKeysForSelector(domain: string, selector: string): Promise<{ privateKey: string; publicKey: string }> {
|
||||
// Try to read from storage manager first
|
||||
if (this.storageManager) {
|
||||
if (hasStorageManagerMethods(this.storageManager, ['get', 'set'])) {
|
||||
try {
|
||||
const [privateKey, publicKey] = await Promise.all([
|
||||
this.storageManager.get(`/email/dkim/${domain}/${selector}/private.key`),
|
||||
@@ -330,6 +345,10 @@ export class DKIMCreator {
|
||||
if (privateKey && publicKey) {
|
||||
return { privateKey, publicKey };
|
||||
}
|
||||
|
||||
if (selector === 'default') {
|
||||
return await this.readDKIMKeys(domain);
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall through to migration check
|
||||
}
|
||||
@@ -347,10 +366,7 @@ export class DKIMCreator {
|
||||
|
||||
// Migrate to storage manager
|
||||
console.log(`Migrating DKIM keys for ${domain}/${selector} from filesystem to StorageManager`);
|
||||
await Promise.all([
|
||||
this.storageManager.set(`/email/dkim/${domain}/${selector}/private.key`, privateKey),
|
||||
this.storageManager.set(`/email/dkim/${domain}/${selector}/public.key`, publicKey)
|
||||
]);
|
||||
await this.storeSelectorKeysToStorage(domain, selector, privateKey, publicKey);
|
||||
|
||||
return { privateKey, publicKey };
|
||||
} catch (error) {
|
||||
@@ -361,6 +377,9 @@ export class DKIMCreator {
|
||||
}
|
||||
} else {
|
||||
// No storage manager, use filesystem directly
|
||||
if (selector === 'default') {
|
||||
return this.readDKIMKeys(domain);
|
||||
}
|
||||
const keyPaths = await this.getKeyPathsForSelector(domain, selector);
|
||||
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
|
||||
readFile(keyPaths.privateKeyPath),
|
||||
@@ -406,7 +425,8 @@ export class DKIMCreator {
|
||||
* Clean up old DKIM keys after grace period
|
||||
*/
|
||||
public async cleanupOldKeys(domain: string, gracePeriodDays: number = 30): Promise<void> {
|
||||
if (!this.storageManager) {
|
||||
if (!hasStorageManagerMethods(this.storageManager, ['get', 'list', 'delete'])) {
|
||||
console.log(`StorageManager for ${domain} does not support list/delete. Skipping DKIM cleanup.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,7 +456,11 @@ export class DKIMCreator {
|
||||
console.warn(`Failed to delete old key files: ${error.message}`);
|
||||
}
|
||||
|
||||
// Delete metadata
|
||||
// Delete selector-specific storage keys and metadata
|
||||
await Promise.all([
|
||||
this.storageManager.delete(`/email/dkim/${domain}/${metadata.selector}/private.key`),
|
||||
this.storageManager.delete(`/email/dkim/${domain}/${metadata.selector}/public.key`),
|
||||
]);
|
||||
await this.storageManager.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -444,4 +468,4 @@ export class DKIMCreator {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user