@realtimex/email-automator 2.8.4 → 2.9.0
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.
- package/api/src/config/index.ts +4 -2
- package/api/src/services/processor.ts +7 -1
- package/api/src/services/storage.ts +22 -5
- package/api/src/utils/filename.ts +55 -0
- package/dist/api/src/config/index.js +3 -1
- package/dist/api/src/services/processor.js +7 -1
- package/dist/api/src/services/storage.js +21 -5
- package/dist/api/src/utils/filename.js +38 -0
- package/dist/assets/{index-B9bx8Y2-.js → index-fpSQFV7a.js} +20 -20
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/supabase/migrations/20260119000003_add_intelligent_rename.sql +5 -0
package/api/src/config/index.ts
CHANGED
|
@@ -39,7 +39,7 @@ function loadEnvironment() {
|
|
|
39
39
|
|
|
40
40
|
loadEnvironment();
|
|
41
41
|
|
|
42
|
-
function parseArgs(args: string[]): { port: number | null, noUi: boolean } {
|
|
42
|
+
function parseArgs(args: string[]): { port: number | null, noUi: boolean, rename: boolean } {
|
|
43
43
|
const portIndex = args.indexOf('--port');
|
|
44
44
|
let port = null;
|
|
45
45
|
if (portIndex !== -1 && args[portIndex + 1]) {
|
|
@@ -50,8 +50,9 @@ function parseArgs(args: string[]): { port: number | null, noUi: boolean } {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const noUi = args.includes('--no-ui');
|
|
53
|
+
const rename = args.includes('--rename');
|
|
53
54
|
|
|
54
|
-
return { port, noUi };
|
|
55
|
+
return { port, noUi, rename };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const cliArgs = parseArgs(process.argv.slice(2));
|
|
@@ -62,6 +63,7 @@ export const config = {
|
|
|
62
63
|
// Default port 3004 (RealTimeX Desktop uses 3001/3002)
|
|
63
64
|
port: cliArgs.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 3004),
|
|
64
65
|
noUi: cliArgs.noUi,
|
|
66
|
+
intelligentRename: cliArgs.rename || process.env.INTELLIGENT_RENAME === 'true',
|
|
65
67
|
nodeEnv: process.env.NODE_ENV || 'development',
|
|
66
68
|
isProduction: process.env.NODE_ENV === 'production',
|
|
67
69
|
|
|
@@ -6,6 +6,7 @@ import { getGmailService, GmailMessage } from './gmail.js';
|
|
|
6
6
|
import { getMicrosoftService, OutlookMessage } from './microsoft.js';
|
|
7
7
|
import { getIntelligenceService, EmailAnalysis, ContextAwareAnalysis, RuleContext } from './intelligence.js';
|
|
8
8
|
import { getStorageService } from './storage.js';
|
|
9
|
+
import { generateEmailFilename } from '../utils/filename.js';
|
|
9
10
|
import { EmailAccount, Email, Rule, ProcessingLog } from './supabase.js';
|
|
10
11
|
import { EventLogger } from './eventLogger.js';
|
|
11
12
|
|
|
@@ -434,7 +435,12 @@ export class EmailProcessorService {
|
|
|
434
435
|
// 2. Save raw content to local storage (.eml format)
|
|
435
436
|
let filePath = '';
|
|
436
437
|
try {
|
|
437
|
-
const filename =
|
|
438
|
+
const filename = generateEmailFilename({
|
|
439
|
+
subject,
|
|
440
|
+
date: parsed.date || new Date(),
|
|
441
|
+
externalId: message.id,
|
|
442
|
+
intelligentRename: settings?.intelligent_rename || config.intelligentRename
|
|
443
|
+
});
|
|
438
444
|
filePath = await this.storageService.saveEmail(rawMime, filename, settings?.storage_path);
|
|
439
445
|
} catch (storageError) {
|
|
440
446
|
logger.error('Failed to save raw email content', storageError);
|
|
@@ -9,16 +9,26 @@ export class StorageService {
|
|
|
9
9
|
private defaultPath: string;
|
|
10
10
|
|
|
11
11
|
constructor() {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// Determine a safe default path
|
|
13
|
+
const homeDir = os.homedir();
|
|
14
|
+
const fallbackPath = path.join(homeDir, '.email-automator', 'emails');
|
|
15
|
+
const projectDataPath = path.resolve(process.cwd(), 'data', 'emails');
|
|
16
|
+
|
|
17
|
+
// If we are at system root or in a restricted environment, use home dir
|
|
18
|
+
if (process.cwd() === '/' || process.cwd() === '/root' || process.cwd().startsWith('/bin')) {
|
|
19
|
+
this.defaultPath = fallbackPath;
|
|
20
|
+
} else {
|
|
21
|
+
// Default to project-relative for now, but ensureDirectory will handle the check
|
|
22
|
+
this.defaultPath = projectDataPath;
|
|
23
|
+
}
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* Ensures the storage directory exists and is writable.
|
|
19
28
|
*/
|
|
20
29
|
async ensureDirectory(customPath?: string | null): Promise<string> {
|
|
21
|
-
|
|
30
|
+
let targetPath = customPath || this.defaultPath;
|
|
31
|
+
|
|
22
32
|
try {
|
|
23
33
|
await fs.mkdir(targetPath, { recursive: true });
|
|
24
34
|
// Test writability
|
|
@@ -27,8 +37,15 @@ export class StorageService {
|
|
|
27
37
|
await fs.unlink(testFile);
|
|
28
38
|
return targetPath;
|
|
29
39
|
} catch (error) {
|
|
40
|
+
// If the default project-relative path failed and we haven't tried the fallback yet
|
|
41
|
+
if (!customPath && targetPath !== path.join(os.homedir(), '.email-automator', 'emails')) {
|
|
42
|
+
const fallback = path.join(os.homedir(), '.email-automator', 'emails');
|
|
43
|
+
logger.warn('Default storage path not writable, falling back to home directory', { targetPath, fallback });
|
|
44
|
+
return this.ensureDirectory(fallback);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
logger.error('Storage directory validation failed', error, { targetPath });
|
|
31
|
-
throw new Error(`Storage path "${targetPath}" is not accessible or writable.`);
|
|
48
|
+
throw new Error(`Storage path "${targetPath}" is not accessible or writable. Please configure a valid path in Settings.`);
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for generating standardized and human-readable filenames for archived emails.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface FilenameOptions {
|
|
6
|
+
subject: string;
|
|
7
|
+
date: Date;
|
|
8
|
+
externalId: string;
|
|
9
|
+
intelligentRename?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function generateEmailFilename({
|
|
13
|
+
subject,
|
|
14
|
+
date,
|
|
15
|
+
externalId,
|
|
16
|
+
intelligentRename = false
|
|
17
|
+
}: FilenameOptions): string {
|
|
18
|
+
// 1. Format Timestamp (YYYYMMDD_HHMM or YYYYMMDD-HHMM)
|
|
19
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
20
|
+
const yyyy = date.getFullYear();
|
|
21
|
+
const mm = pad(date.getMonth() + 1);
|
|
22
|
+
const dd = pad(date.getDate());
|
|
23
|
+
const hh = pad(date.getHours());
|
|
24
|
+
const min = pad(date.getMinutes());
|
|
25
|
+
|
|
26
|
+
const timestamp = intelligentRename
|
|
27
|
+
? `${yyyy}${mm}${dd}-${hh}${min}`
|
|
28
|
+
: `${yyyy}${mm}${dd}_${hh}${min}`;
|
|
29
|
+
|
|
30
|
+
// 2. Get Internal ID (Last 8 characters of provider's message ID)
|
|
31
|
+
const internalId = externalId.slice(-8);
|
|
32
|
+
|
|
33
|
+
if (intelligentRename) {
|
|
34
|
+
// Intelligent Rename (Slugified)
|
|
35
|
+
// subject converted to lowercase, non-alphanumeric to hyphens
|
|
36
|
+
const slug = subject
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
40
|
+
.substring(0, 100);
|
|
41
|
+
|
|
42
|
+
return `${timestamp}-${slug || 'no-subject'}-${internalId}.eml`;
|
|
43
|
+
} else {
|
|
44
|
+
// Default Naming Convention
|
|
45
|
+
// Illegal characters replaced with underscores, non-printable removed
|
|
46
|
+
const sanitized = subject
|
|
47
|
+
.replace(/[/\\*?:\"<>|]/g, '_') // Illegal chars
|
|
48
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // Non-printable
|
|
49
|
+
.replace(/_+/g, '_') // Collapse multiple underscores
|
|
50
|
+
.replace(/^_|_$/g, '') // Remove leading/trailing underscores
|
|
51
|
+
.substring(0, 100);
|
|
52
|
+
|
|
53
|
+
return `${timestamp}_${sanitized || 'No_Subject'}_${internalId}.eml`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -44,7 +44,8 @@ function parseArgs(args) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
const noUi = args.includes('--no-ui');
|
|
47
|
-
|
|
47
|
+
const rename = args.includes('--rename');
|
|
48
|
+
return { port, noUi, rename };
|
|
48
49
|
}
|
|
49
50
|
const cliArgs = parseArgs(process.argv.slice(2));
|
|
50
51
|
export const config = {
|
|
@@ -53,6 +54,7 @@ export const config = {
|
|
|
53
54
|
// Default port 3004 (RealTimeX Desktop uses 3001/3002)
|
|
54
55
|
port: cliArgs.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 3004),
|
|
55
56
|
noUi: cliArgs.noUi,
|
|
57
|
+
intelligentRename: cliArgs.rename || process.env.INTELLIGENT_RENAME === 'true',
|
|
56
58
|
nodeEnv: process.env.NODE_ENV || 'development',
|
|
57
59
|
isProduction: process.env.NODE_ENV === 'production',
|
|
58
60
|
// Paths - Robust resolution for both TS source and compiled JS in dist/
|
|
@@ -5,6 +5,7 @@ import { getGmailService } from './gmail.js';
|
|
|
5
5
|
import { getMicrosoftService } from './microsoft.js';
|
|
6
6
|
import { getIntelligenceService } from './intelligence.js';
|
|
7
7
|
import { getStorageService } from './storage.js';
|
|
8
|
+
import { generateEmailFilename } from '../utils/filename.js';
|
|
8
9
|
import { EventLogger } from './eventLogger.js';
|
|
9
10
|
const logger = createLogger('Processor');
|
|
10
11
|
export class EmailProcessorService {
|
|
@@ -352,7 +353,12 @@ export class EmailProcessorService {
|
|
|
352
353
|
// 2. Save raw content to local storage (.eml format)
|
|
353
354
|
let filePath = '';
|
|
354
355
|
try {
|
|
355
|
-
const filename =
|
|
356
|
+
const filename = generateEmailFilename({
|
|
357
|
+
subject,
|
|
358
|
+
date: parsed.date || new Date(),
|
|
359
|
+
externalId: message.id,
|
|
360
|
+
intelligentRename: settings?.intelligent_rename || config.intelligentRename
|
|
361
|
+
});
|
|
356
362
|
filePath = await this.storageService.saveEmail(rawMime, filename, settings?.storage_path);
|
|
357
363
|
}
|
|
358
364
|
catch (storageError) {
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { createLogger } from '../utils/logger.js';
|
|
4
5
|
const logger = createLogger('StorageService');
|
|
5
6
|
export class StorageService {
|
|
6
7
|
defaultPath;
|
|
7
8
|
constructor() {
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Determine a safe default path
|
|
10
|
+
const homeDir = os.homedir();
|
|
11
|
+
const fallbackPath = path.join(homeDir, '.email-automator', 'emails');
|
|
12
|
+
const projectDataPath = path.resolve(process.cwd(), 'data', 'emails');
|
|
13
|
+
// If we are at system root or in a restricted environment, use home dir
|
|
14
|
+
if (process.cwd() === '/' || process.cwd() === '/root' || process.cwd().startsWith('/bin')) {
|
|
15
|
+
this.defaultPath = fallbackPath;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// Default to project-relative for now, but ensureDirectory will handle the check
|
|
19
|
+
this.defaultPath = projectDataPath;
|
|
20
|
+
}
|
|
11
21
|
}
|
|
12
22
|
/**
|
|
13
23
|
* Ensures the storage directory exists and is writable.
|
|
14
24
|
*/
|
|
15
25
|
async ensureDirectory(customPath) {
|
|
16
|
-
|
|
26
|
+
let targetPath = customPath || this.defaultPath;
|
|
17
27
|
try {
|
|
18
28
|
await fs.mkdir(targetPath, { recursive: true });
|
|
19
29
|
// Test writability
|
|
@@ -23,8 +33,14 @@ export class StorageService {
|
|
|
23
33
|
return targetPath;
|
|
24
34
|
}
|
|
25
35
|
catch (error) {
|
|
36
|
+
// If the default project-relative path failed and we haven't tried the fallback yet
|
|
37
|
+
if (!customPath && targetPath !== path.join(os.homedir(), '.email-automator', 'emails')) {
|
|
38
|
+
const fallback = path.join(os.homedir(), '.email-automator', 'emails');
|
|
39
|
+
logger.warn('Default storage path not writable, falling back to home directory', { targetPath, fallback });
|
|
40
|
+
return this.ensureDirectory(fallback);
|
|
41
|
+
}
|
|
26
42
|
logger.error('Storage directory validation failed', error, { targetPath });
|
|
27
|
-
throw new Error(`Storage path "${targetPath}" is not accessible or writable.`);
|
|
43
|
+
throw new Error(`Storage path "${targetPath}" is not accessible or writable. Please configure a valid path in Settings.`);
|
|
28
44
|
}
|
|
29
45
|
}
|
|
30
46
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for generating standardized and human-readable filenames for archived emails.
|
|
3
|
+
*/
|
|
4
|
+
export function generateEmailFilename({ subject, date, externalId, intelligentRename = false }) {
|
|
5
|
+
// 1. Format Timestamp (YYYYMMDD_HHMM or YYYYMMDD-HHMM)
|
|
6
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
7
|
+
const yyyy = date.getFullYear();
|
|
8
|
+
const mm = pad(date.getMonth() + 1);
|
|
9
|
+
const dd = pad(date.getDate());
|
|
10
|
+
const hh = pad(date.getHours());
|
|
11
|
+
const min = pad(date.getMinutes());
|
|
12
|
+
const timestamp = intelligentRename
|
|
13
|
+
? `${yyyy}${mm}${dd}-${hh}${min}`
|
|
14
|
+
: `${yyyy}${mm}${dd}_${hh}${min}`;
|
|
15
|
+
// 2. Get Internal ID (Last 8 characters of provider's message ID)
|
|
16
|
+
const internalId = externalId.slice(-8);
|
|
17
|
+
if (intelligentRename) {
|
|
18
|
+
// Intelligent Rename (Slugified)
|
|
19
|
+
// subject converted to lowercase, non-alphanumeric to hyphens
|
|
20
|
+
const slug = subject
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
24
|
+
.substring(0, 100);
|
|
25
|
+
return `${timestamp}-${slug || 'no-subject'}-${internalId}.eml`;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Default Naming Convention
|
|
29
|
+
// Illegal characters replaced with underscores, non-printable removed
|
|
30
|
+
const sanitized = subject
|
|
31
|
+
.replace(/[/\\*?:\"<>|]/g, '_') // Illegal chars
|
|
32
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // Non-printable
|
|
33
|
+
.replace(/_+/g, '_') // Collapse multiple underscores
|
|
34
|
+
.replace(/^_|_$/g, '') // Remove leading/trailing underscores
|
|
35
|
+
.substring(0, 100);
|
|
36
|
+
return `${timestamp}_${sanitized || 'No_Subject'}_${internalId}.eml`;
|
|
37
|
+
}
|
|
38
|
+
}
|