@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.
@@ -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 = `${account.id}_${message.id}.eml`.replace(/[^a-z0-9._-]/gi, '_');
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
- // Default to a folder in the user's home directory or current project
13
- // Using project-relative path for now as discussed
14
- this.defaultPath = path.resolve(process.cwd(), 'data', 'emails');
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
- const targetPath = customPath || this.defaultPath;
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
- return { port, noUi };
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 = `${account.id}_${message.id}.eml`.replace(/[^a-z0-9._-]/gi, '_');
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
- // Default to a folder in the user's home directory or current project
9
- // Using project-relative path for now as discussed
10
- this.defaultPath = path.resolve(process.cwd(), 'data', 'emails');
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
- const targetPath = customPath || this.defaultPath;
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
+ }