@push.rocks/smartmta 5.1.3 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/changelog.md +15 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +3 -0
  5. package/dist_ts/index.js +4 -0
  6. package/dist_ts/logger.d.ts +17 -0
  7. package/dist_ts/logger.js +76 -0
  8. package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
  9. package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
  10. package/dist_ts/mail/core/classes.email.d.ts +291 -0
  11. package/dist_ts/mail/core/classes.email.js +802 -0
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
  13. package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
  15. package/dist_ts/mail/core/classes.templatemanager.js +240 -0
  16. package/dist_ts/mail/core/index.d.ts +4 -0
  17. package/dist_ts/mail/core/index.js +6 -0
  18. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
  19. package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
  20. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
  21. package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
  22. package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
  23. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
  24. package/dist_ts/mail/delivery/index.d.ts +4 -0
  25. package/dist_ts/mail/delivery/index.js +6 -0
  26. package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
  27. package/dist_ts/mail/delivery/interfaces.js +17 -0
  28. package/dist_ts/mail/index.d.ts +7 -0
  29. package/dist_ts/mail/index.js +12 -0
  30. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
  31. package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
  32. package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
  33. package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
  34. package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
  35. package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
  36. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
  37. package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
  38. package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
  39. package/dist_ts/mail/routing/classes.email.router.js +494 -0
  40. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
  41. package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
  42. package/dist_ts/mail/routing/index.d.ts +7 -0
  43. package/dist_ts/mail/routing/index.js +9 -0
  44. package/dist_ts/mail/routing/interfaces.d.ts +187 -0
  45. package/dist_ts/mail/routing/interfaces.js +2 -0
  46. package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
  47. package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
  48. package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
  49. package/dist_ts/mail/security/classes.spfverifier.js +87 -0
  50. package/dist_ts/mail/security/index.d.ts +2 -0
  51. package/dist_ts/mail/security/index.js +4 -0
  52. package/dist_ts/paths.d.ts +14 -0
  53. package/dist_ts/paths.js +39 -0
  54. package/dist_ts/plugins.d.ts +24 -0
  55. package/dist_ts/plugins.js +28 -0
  56. package/dist_ts/security/classes.contentscanner.d.ts +130 -0
  57. package/dist_ts/security/classes.contentscanner.js +338 -0
  58. package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
  59. package/dist_ts/security/classes.ipreputationchecker.js +263 -0
  60. package/dist_ts/security/classes.rustsecuritybridge.d.ts +403 -0
  61. package/dist_ts/security/classes.rustsecuritybridge.js +502 -0
  62. package/dist_ts/security/classes.securitylogger.d.ts +140 -0
  63. package/dist_ts/security/classes.securitylogger.js +235 -0
  64. package/dist_ts/security/index.d.ts +4 -0
  65. package/dist_ts/security/index.js +5 -0
  66. package/package.json +6 -1
  67. package/ts/00_commitinfo_data.ts +8 -0
  68. package/ts/index.ts +3 -0
  69. package/ts/logger.ts +91 -0
  70. package/ts/mail/core/classes.bouncemanager.ts +731 -0
  71. package/ts/mail/core/classes.email.ts +942 -0
  72. package/ts/mail/core/classes.emailvalidator.ts +239 -0
  73. package/ts/mail/core/classes.templatemanager.ts +320 -0
  74. package/ts/mail/core/index.ts +5 -0
  75. package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
  76. package/ts/mail/delivery/classes.delivery.system.ts +816 -0
  77. package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
  78. package/ts/mail/delivery/index.ts +5 -0
  79. package/ts/mail/delivery/interfaces.ts +167 -0
  80. package/ts/mail/index.ts +17 -0
  81. package/ts/mail/routing/classes.dkim.manager.ts +157 -0
  82. package/ts/mail/routing/classes.dns.manager.ts +573 -0
  83. package/ts/mail/routing/classes.domain.registry.ts +139 -0
  84. package/ts/mail/routing/classes.email.action.executor.ts +175 -0
  85. package/ts/mail/routing/classes.email.router.ts +575 -0
  86. package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
  87. package/ts/mail/routing/index.ts +9 -0
  88. package/ts/mail/routing/interfaces.ts +202 -0
  89. package/ts/mail/security/classes.dkimcreator.ts +447 -0
  90. package/ts/mail/security/classes.spfverifier.ts +126 -0
  91. package/ts/mail/security/index.ts +3 -0
  92. package/ts/paths.ts +48 -0
  93. package/ts/plugins.ts +53 -0
  94. package/ts/security/classes.contentscanner.ts +400 -0
  95. package/ts/security/classes.ipreputationchecker.ts +315 -0
  96. package/ts/security/classes.rustsecuritybridge.ts +964 -0
  97. package/ts/security/classes.securitylogger.ts +299 -0
  98. package/ts/security/index.ts +40 -0
@@ -0,0 +1,126 @@
1
+ import { logger } from '../../logger.js';
2
+
3
+ /**
4
+ * SPF result qualifiers
5
+ */
6
+ export enum SpfQualifier {
7
+ PASS = '+',
8
+ NEUTRAL = '?',
9
+ SOFTFAIL = '~',
10
+ FAIL = '-'
11
+ }
12
+
13
+ /**
14
+ * SPF mechanism types
15
+ */
16
+ export enum SpfMechanismType {
17
+ ALL = 'all',
18
+ INCLUDE = 'include',
19
+ A = 'a',
20
+ MX = 'mx',
21
+ IP4 = 'ip4',
22
+ IP6 = 'ip6',
23
+ EXISTS = 'exists',
24
+ REDIRECT = 'redirect',
25
+ EXP = 'exp'
26
+ }
27
+
28
+ /**
29
+ * SPF mechanism definition
30
+ */
31
+ export interface SpfMechanism {
32
+ qualifier: SpfQualifier;
33
+ type: SpfMechanismType;
34
+ value?: string;
35
+ }
36
+
37
+ /**
38
+ * SPF record parsed data
39
+ */
40
+ export interface SpfRecord {
41
+ version: string;
42
+ mechanisms: SpfMechanism[];
43
+ modifiers: Record<string, string>;
44
+ }
45
+
46
+ /**
47
+ * SPF verification result
48
+ */
49
+ export interface SpfResult {
50
+ result: 'pass' | 'neutral' | 'softfail' | 'fail' | 'temperror' | 'permerror' | 'none';
51
+ explanation?: string;
52
+ domain: string;
53
+ ip: string;
54
+ record?: string;
55
+ error?: string;
56
+ }
57
+
58
+ /**
59
+ * Class for verifying SPF records.
60
+ * Delegates actual SPF evaluation to the Rust security bridge.
61
+ * Retains parseSpfRecord() for lightweight local parsing.
62
+ */
63
+ export class SpfVerifier {
64
+ constructor(_dnsManager?: any) {
65
+ // dnsManager is no longer needed — Rust handles DNS lookups
66
+ }
67
+
68
+ /**
69
+ * Parse SPF record from TXT record (pure string parsing, no DNS)
70
+ */
71
+ public parseSpfRecord(record: string): SpfRecord | null {
72
+ if (!record.startsWith('v=spf1')) {
73
+ return null;
74
+ }
75
+
76
+ try {
77
+ const spfRecord: SpfRecord = {
78
+ version: 'spf1',
79
+ mechanisms: [],
80
+ modifiers: {}
81
+ };
82
+
83
+ const terms = record.split(' ').filter(term => term.length > 0);
84
+
85
+ for (let i = 1; i < terms.length; i++) {
86
+ const term = terms[i];
87
+
88
+ if (term.includes('=')) {
89
+ const [name, value] = term.split('=');
90
+ spfRecord.modifiers[name] = value;
91
+ continue;
92
+ }
93
+
94
+ let qualifier = SpfQualifier.PASS;
95
+ let mechanismText = term;
96
+
97
+ if (term.startsWith('+') || term.startsWith('-') ||
98
+ term.startsWith('~') || term.startsWith('?')) {
99
+ qualifier = term[0] as SpfQualifier;
100
+ mechanismText = term.substring(1);
101
+ }
102
+
103
+ const colonIndex = mechanismText.indexOf(':');
104
+ let type: SpfMechanismType;
105
+ let value: string | undefined;
106
+
107
+ if (colonIndex !== -1) {
108
+ type = mechanismText.substring(0, colonIndex) as SpfMechanismType;
109
+ value = mechanismText.substring(colonIndex + 1);
110
+ } else {
111
+ type = mechanismText as SpfMechanismType;
112
+ }
113
+
114
+ spfRecord.mechanisms.push({ qualifier, type, value });
115
+ }
116
+
117
+ return spfRecord;
118
+ } catch (error) {
119
+ logger.log('error', `Error parsing SPF record: ${error.message}`, {
120
+ record,
121
+ error: error.message
122
+ });
123
+ return null;
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,3 @@
1
+ // Email security components
2
+ export * from './classes.dkimcreator.js';
3
+ export * from './classes.spfverifier.js';
package/ts/paths.ts ADDED
@@ -0,0 +1,48 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ // Base directories
4
+ export const baseDir = process.cwd();
5
+ export const packageDir = plugins.path.join(
6
+ plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
7
+ '../'
8
+ );
9
+ export const distServe = plugins.path.join(packageDir, './dist_serve');
10
+
11
+ // Configure data directory with environment variable or default to .nogit/data
12
+ const DEFAULT_DATA_PATH = '.nogit/data';
13
+ export const dataDir = process.env.DATA_DIR
14
+ ? process.env.DATA_DIR
15
+ : plugins.path.join(baseDir, DEFAULT_DATA_PATH);
16
+
17
+ // MTA directories
18
+ export const keysDir = plugins.path.join(dataDir, 'keys');
19
+ export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
20
+ export const sentEmailsDir = plugins.path.join(dataDir, 'emails', 'sent');
21
+ export const receivedEmailsDir = plugins.path.join(dataDir, 'emails', 'received');
22
+ export const failedEmailsDir = plugins.path.join(dataDir, 'emails', 'failed'); // For failed emails
23
+ export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
24
+
25
+ // Email template directories
26
+ export const emailTemplatesDir = plugins.path.join(dataDir, 'templates', 'email');
27
+ export const MtaAttachmentsDir = plugins.path.join(dataDir, 'attachments'); // For email attachments
28
+
29
+ // Configuration path
30
+ export const configPath = process.env.CONFIG_PATH
31
+ ? process.env.CONFIG_PATH
32
+ : plugins.path.join(baseDir, 'config.json');
33
+
34
+ // Create directories if they don't exist
35
+ export async function ensureDirectories() {
36
+ // Ensure data directories
37
+ await plugins.smartfs.directory(dataDir).recursive().create();
38
+ await plugins.smartfs.directory(keysDir).recursive().create();
39
+ await plugins.smartfs.directory(dnsRecordsDir).recursive().create();
40
+ await plugins.smartfs.directory(sentEmailsDir).recursive().create();
41
+ await plugins.smartfs.directory(receivedEmailsDir).recursive().create();
42
+ await plugins.smartfs.directory(failedEmailsDir).recursive().create();
43
+ await plugins.smartfs.directory(logsDir).recursive().create();
44
+
45
+ // Ensure email template directories
46
+ await plugins.smartfs.directory(emailTemplatesDir).recursive().create();
47
+ await plugins.smartfs.directory(MtaAttachmentsDir).recursive().create();
48
+ }
package/ts/plugins.ts ADDED
@@ -0,0 +1,53 @@
1
+ // node native
2
+ import * as dns from 'dns';
3
+ import * as fs from 'fs';
4
+ import * as crypto from 'crypto';
5
+ import * as http from 'http';
6
+ import * as net from 'net';
7
+ import * as os from 'os';
8
+ import * as path from 'path';
9
+ import * as tls from 'tls';
10
+ import * as util from 'util';
11
+
12
+ export {
13
+ dns,
14
+ fs,
15
+ crypto,
16
+ http,
17
+ net,
18
+ os,
19
+ path,
20
+ tls,
21
+ util,
22
+ }
23
+
24
+ // @push.rocks scope
25
+ import * as smartfile from '@push.rocks/smartfile';
26
+ import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
27
+ import * as smartlog from '@push.rocks/smartlog';
28
+ import * as smartmail from '@push.rocks/smartmail';
29
+ import * as smartpath from '@push.rocks/smartpath';
30
+ import * as smartrust from '@push.rocks/smartrust';
31
+
32
+ export const smartfs = new SmartFs(new SmartFsProviderNode());
33
+
34
+ export { smartfile, SmartFs, smartlog, smartmail, smartpath, smartrust };
35
+
36
+ // Define SmartLog types for use in error handling
37
+ export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
38
+
39
+ // tsclass scope
40
+ import * as tsclass from '@tsclass/tsclass';
41
+
42
+ export {
43
+ tsclass,
44
+ }
45
+
46
+ // third party
47
+ import mailparser from 'mailparser';
48
+ import * as uuid from 'uuid';
49
+
50
+ export {
51
+ mailparser,
52
+ uuid,
53
+ }
@@ -0,0 +1,400 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as paths from '../paths.js';
3
+ import { logger } from '../logger.js';
4
+ import { Email } from '../mail/core/classes.email.js';
5
+ import type { IAttachment } from '../mail/core/classes.email.js';
6
+ import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
7
+ import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
8
+ import { LRUCache } from 'lru-cache';
9
+
10
+ /**
11
+ * Scan result information
12
+ */
13
+ export interface IScanResult {
14
+ isClean: boolean; // Whether the content is clean (no threats detected)
15
+ threatType?: string; // Type of threat if detected
16
+ threatDetails?: string; // Details about the detected threat
17
+ threatScore: number; // 0 (clean) to 100 (definitely malicious)
18
+ scannedElements: string[]; // What was scanned (subject, body, attachments, etc.)
19
+ timestamp: number; // When this scan was performed
20
+ }
21
+
22
+ /**
23
+ * Options for content scanner configuration
24
+ */
25
+ export interface IContentScannerOptions {
26
+ maxCacheSize?: number; // Maximum number of entries to cache
27
+ cacheTTL?: number; // TTL for cache entries in ms
28
+ scanSubject?: boolean; // Whether to scan email subjects
29
+ scanBody?: boolean; // Whether to scan email bodies
30
+ scanAttachments?: boolean; // Whether to scan attachments
31
+ maxAttachmentSizeToScan?: number; // Max size of attachments to scan in bytes
32
+ scanAttachmentNames?: boolean; // Whether to scan attachment filenames
33
+ blockExecutables?: boolean; // Whether to block executable attachments
34
+ blockMacros?: boolean; // Whether to block documents with macros
35
+ customRules?: Array<{ // Custom scanning rules
36
+ pattern: string | RegExp; // Pattern to match
37
+ type: string; // Type of threat
38
+ score: number; // Threat score
39
+ description: string; // Description of the threat
40
+ }>;
41
+ minThreatScore?: number; // Minimum score to consider content as a threat
42
+ highThreatScore?: number; // Score above which content is considered high threat
43
+ }
44
+
45
+ /**
46
+ * Threat categories
47
+ */
48
+ export enum ThreatCategory {
49
+ SPAM = 'spam',
50
+ PHISHING = 'phishing',
51
+ MALWARE = 'malware',
52
+ EXECUTABLE = 'executable',
53
+ SUSPICIOUS_LINK = 'suspicious_link',
54
+ MALICIOUS_MACRO = 'malicious_macro',
55
+ XSS = 'xss',
56
+ SENSITIVE_DATA = 'sensitive_data',
57
+ BLACKLISTED_CONTENT = 'blacklisted_content',
58
+ CUSTOM_RULE = 'custom_rule'
59
+ }
60
+
61
+ /**
62
+ * Content Scanner for detecting malicious email content
63
+ */
64
+ export class ContentScanner {
65
+ private static instance: ContentScanner;
66
+ private scanCache: LRUCache<string, IScanResult>;
67
+ private options: Required<IContentScannerOptions>;
68
+
69
+ /**
70
+ * Default options for the content scanner
71
+ */
72
+ private static readonly DEFAULT_OPTIONS: Required<IContentScannerOptions> = {
73
+ maxCacheSize: 10000,
74
+ cacheTTL: 24 * 60 * 60 * 1000, // 24 hours
75
+ scanSubject: true,
76
+ scanBody: true,
77
+ scanAttachments: true,
78
+ maxAttachmentSizeToScan: 10 * 1024 * 1024, // 10MB
79
+ scanAttachmentNames: true,
80
+ blockExecutables: true,
81
+ blockMacros: true,
82
+ customRules: [],
83
+ minThreatScore: 30, // Minimum score to consider content as a threat
84
+ highThreatScore: 70 // Score above which content is considered high threat
85
+ };
86
+
87
+ /**
88
+ * Constructor for the ContentScanner
89
+ * @param options Configuration options
90
+ */
91
+ constructor(options: IContentScannerOptions = {}) {
92
+ // Merge with default options
93
+ this.options = {
94
+ ...ContentScanner.DEFAULT_OPTIONS,
95
+ ...options
96
+ };
97
+
98
+ // Initialize cache
99
+ this.scanCache = new LRUCache<string, IScanResult>({
100
+ max: this.options.maxCacheSize,
101
+ ttl: this.options.cacheTTL,
102
+ });
103
+
104
+ logger.log('info', 'ContentScanner initialized');
105
+ }
106
+
107
+ /**
108
+ * Get the singleton instance of the scanner
109
+ * @param options Configuration options
110
+ * @returns Singleton scanner instance
111
+ */
112
+ public static getInstance(options: IContentScannerOptions = {}): ContentScanner {
113
+ if (!ContentScanner.instance) {
114
+ ContentScanner.instance = new ContentScanner(options);
115
+ }
116
+ return ContentScanner.instance;
117
+ }
118
+
119
+ /**
120
+ * Scan an email for malicious content.
121
+ * Delegates text/subject/html/filename pattern scanning to Rust.
122
+ * Binary attachment scanning (PE headers, VBA macros) stays in TS.
123
+ * @param email The email to scan
124
+ * @returns Scan result
125
+ */
126
+ public async scanEmail(email: Email): Promise<IScanResult> {
127
+ try {
128
+ // Generate a cache key from the email
129
+ const cacheKey = this.generateCacheKey(email);
130
+
131
+ // Check cache first
132
+ const cachedResult = this.scanCache.get(cacheKey);
133
+ if (cachedResult) {
134
+ logger.log('info', `Using cached scan result for email ${email.getMessageId()}`);
135
+ return cachedResult;
136
+ }
137
+
138
+ // Delegate text/subject/html/filename scanning to Rust
139
+ const bridge = RustSecurityBridge.getInstance();
140
+ const rustResult = await bridge.scanContent({
141
+ subject: this.options.scanSubject ? email.subject : undefined,
142
+ textBody: this.options.scanBody ? email.text : undefined,
143
+ htmlBody: this.options.scanBody ? email.html : undefined,
144
+ attachmentNames: this.options.scanAttachmentNames
145
+ ? email.attachments?.map(a => a.filename) ?? []
146
+ : [],
147
+ });
148
+
149
+ const result: IScanResult = {
150
+ isClean: true,
151
+ threatScore: rustResult.threatScore,
152
+ threatType: rustResult.threatType ?? undefined,
153
+ threatDetails: rustResult.threatDetails ?? undefined,
154
+ scannedElements: rustResult.scannedElements,
155
+ timestamp: Date.now(),
156
+ };
157
+
158
+ // Attachment binary scanning stays in TS (PE headers, macro detection)
159
+ if (this.options.scanAttachments && email.attachments?.length > 0) {
160
+ for (const attachment of email.attachments) {
161
+ this.scanAttachmentBinary(attachment, result);
162
+ }
163
+ }
164
+
165
+ // Apply custom rules (TS-only, runtime-configured)
166
+ this.applyCustomRules(email, result);
167
+
168
+ // Determine if the email is clean based on threat score
169
+ result.isClean = result.threatScore < this.options.minThreatScore;
170
+
171
+ // Save to cache
172
+ this.scanCache.set(cacheKey, result);
173
+
174
+ // Log high threat findings
175
+ if (result.threatScore >= this.options.highThreatScore) {
176
+ this.logHighThreatFound(email, result);
177
+ } else if (!result.isClean) {
178
+ this.logThreatFound(email, result);
179
+ }
180
+
181
+ return result;
182
+ } catch (error) {
183
+ logger.log('error', `Error scanning email: ${error.message}`, {
184
+ messageId: email.getMessageId(),
185
+ error: error.stack
186
+ });
187
+
188
+ // Return a safe default with error indication
189
+ return {
190
+ isClean: true,
191
+ threatScore: 0,
192
+ scannedElements: ['error'],
193
+ timestamp: Date.now(),
194
+ threatType: 'scan_error',
195
+ threatDetails: `Scan error: ${error.message}`
196
+ };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Generate a cache key from an email
202
+ * @param email The email to generate a key for
203
+ * @returns Cache key
204
+ */
205
+ private generateCacheKey(email: Email): string {
206
+ // Use message ID if available
207
+ if (email.getMessageId()) {
208
+ return `email:${email.getMessageId()}`;
209
+ }
210
+
211
+ // Fallback to a hash of key content
212
+ const contentToHash = [
213
+ email.from,
214
+ email.subject || '',
215
+ email.text?.substring(0, 1000) || '',
216
+ email.html?.substring(0, 1000) || '',
217
+ email.attachments?.length || 0
218
+ ].join(':');
219
+
220
+ return `email:${plugins.crypto.createHash('sha256').update(contentToHash).digest('hex')}`;
221
+ }
222
+
223
+ /**
224
+ * Scan attachment binary content for PE headers and VBA macros.
225
+ * This stays in TS because it accesses raw Buffer data (too large for IPC).
226
+ * @param attachment The attachment to scan
227
+ * @param result The scan result to update
228
+ */
229
+ private scanAttachmentBinary(attachment: IAttachment, result: IScanResult): void {
230
+ if (!attachment.content) {
231
+ return;
232
+ }
233
+
234
+ // Skip large attachments
235
+ if (attachment.content.length > this.options.maxAttachmentSizeToScan) {
236
+ return;
237
+ }
238
+
239
+ const filename = attachment.filename.toLowerCase();
240
+
241
+ // Check for PE headers (Windows executables disguised with non-.exe extensions)
242
+ if (attachment.content.length > 64 &&
243
+ attachment.content[0] === 0x4D &&
244
+ attachment.content[1] === 0x5A) { // 'MZ' header
245
+ result.threatScore += 80;
246
+ result.threatType = ThreatCategory.EXECUTABLE;
247
+ result.threatDetails = `Attachment contains executable code: ${filename}`;
248
+ return;
249
+ }
250
+
251
+ // Check for VBA macro indicators in Office documents
252
+ if (this.options.blockMacros && this.likelyContainsMacros(attachment)) {
253
+ result.threatScore += 60;
254
+ result.threatType = ThreatCategory.MALICIOUS_MACRO;
255
+ result.threatDetails = `Attachment appears to contain macros: ${filename}`;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Apply custom rules (runtime-configured patterns) to the email.
261
+ * These stay in TS because they are configured at runtime.
262
+ * @param email The email to check
263
+ * @param result The scan result to update
264
+ */
265
+ private applyCustomRules(email: Email, result: IScanResult): void {
266
+ if (!this.options.customRules.length) {
267
+ return;
268
+ }
269
+
270
+ const textsToCheck: string[] = [];
271
+ if (email.subject) textsToCheck.push(email.subject);
272
+ if (email.text) textsToCheck.push(email.text);
273
+ if (email.html) textsToCheck.push(email.html);
274
+
275
+ for (const rule of this.options.customRules) {
276
+ const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern, 'i');
277
+ for (const text of textsToCheck) {
278
+ if (pattern.test(text)) {
279
+ result.threatScore += rule.score;
280
+ result.threatType = rule.type;
281
+ result.threatDetails = rule.description;
282
+ return;
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Extract text from a binary buffer for scanning
290
+ * @param buffer Binary content
291
+ * @returns Extracted text (may be partial)
292
+ */
293
+ private extractTextFromBuffer(buffer: Buffer): string {
294
+ try {
295
+ // Limit the amount we convert to avoid memory issues
296
+ const sampleSize = Math.min(buffer.length, 100 * 1024); // 100KB max sample
297
+ const sample = buffer.slice(0, sampleSize);
298
+
299
+ // Try to convert to string, filtering out non-printable chars
300
+ return sample.toString('utf8')
301
+ .replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '') // Remove control chars
302
+ .replace(/\uFFFD/g, ''); // Remove replacement char
303
+ } catch (error) {
304
+ logger.log('warn', `Error extracting text from buffer: ${error.message}`);
305
+ return '';
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Check if an Office document likely contains macros
311
+ * @param attachment The attachment to check
312
+ * @returns Whether the file likely contains macros
313
+ */
314
+ private likelyContainsMacros(attachment: IAttachment): boolean {
315
+ const content = this.extractTextFromBuffer(attachment.content);
316
+ const macroIndicators = [
317
+ /vbaProject\.bin/i,
318
+ /Microsoft VBA/i,
319
+ /\bVBA\b/,
320
+ /Auto_Open/i,
321
+ /AutoExec/i,
322
+ /DocumentOpen/i,
323
+ /AutoOpen/i,
324
+ /\bExecute\(/i,
325
+ /\bShell\(/i,
326
+ /\bCreateObject\(/i
327
+ ];
328
+
329
+ for (const indicator of macroIndicators) {
330
+ if (indicator.test(content)) {
331
+ return true;
332
+ }
333
+ }
334
+
335
+ return false;
336
+ }
337
+
338
+ /**
339
+ * Log a high threat finding to the security logger
340
+ * @param email The email containing the threat
341
+ * @param result The scan result
342
+ */
343
+ private logHighThreatFound(email: Email, result: IScanResult): void {
344
+ SecurityLogger.getInstance().logEvent({
345
+ level: SecurityLogLevel.ERROR,
346
+ type: SecurityEventType.MALWARE,
347
+ message: `High threat content detected in email from ${email.from} to ${email.to.join(', ')}`,
348
+ details: {
349
+ messageId: email.getMessageId(),
350
+ threatType: result.threatType,
351
+ threatDetails: result.threatDetails,
352
+ threatScore: result.threatScore,
353
+ scannedElements: result.scannedElements,
354
+ subject: email.subject
355
+ },
356
+ success: false,
357
+ domain: email.getFromDomain()
358
+ });
359
+ }
360
+
361
+ /**
362
+ * Log a threat finding to the security logger
363
+ * @param email The email containing the threat
364
+ * @param result The scan result
365
+ */
366
+ private logThreatFound(email: Email, result: IScanResult): void {
367
+ SecurityLogger.getInstance().logEvent({
368
+ level: SecurityLogLevel.WARN,
369
+ type: SecurityEventType.SPAM,
370
+ message: `Suspicious content detected in email from ${email.from} to ${email.to.join(', ')}`,
371
+ details: {
372
+ messageId: email.getMessageId(),
373
+ threatType: result.threatType,
374
+ threatDetails: result.threatDetails,
375
+ threatScore: result.threatScore,
376
+ scannedElements: result.scannedElements,
377
+ subject: email.subject
378
+ },
379
+ success: false,
380
+ domain: email.getFromDomain()
381
+ });
382
+ }
383
+
384
+ /**
385
+ * Get threat level description based on score
386
+ * @param score Threat score
387
+ * @returns Threat level description
388
+ */
389
+ public static getThreatLevel(score: number): 'none' | 'low' | 'medium' | 'high' {
390
+ if (score < 20) {
391
+ return 'none';
392
+ } else if (score < 40) {
393
+ return 'low';
394
+ } else if (score < 70) {
395
+ return 'medium';
396
+ } else {
397
+ return 'high';
398
+ }
399
+ }
400
+ }