email-origin-chain 1.0.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/dist/detectors/crisp-detector.d.ts +11 -0
  4. package/dist/detectors/crisp-detector.js +46 -0
  5. package/dist/detectors/index.d.ts +5 -0
  6. package/dist/detectors/index.js +11 -0
  7. package/dist/detectors/new-outlook-detector.d.ts +10 -0
  8. package/dist/detectors/new-outlook-detector.js +112 -0
  9. package/dist/detectors/outlook-empty-header-detector.d.ts +16 -0
  10. package/dist/detectors/outlook-empty-header-detector.js +64 -0
  11. package/dist/detectors/outlook-fr-detector.d.ts +10 -0
  12. package/dist/detectors/outlook-fr-detector.js +119 -0
  13. package/dist/detectors/outlook-reverse-fr-detector.d.ts +13 -0
  14. package/dist/detectors/outlook-reverse-fr-detector.js +86 -0
  15. package/dist/detectors/registry.d.ts +25 -0
  16. package/dist/detectors/registry.js +81 -0
  17. package/dist/detectors/reply-detector.d.ts +11 -0
  18. package/dist/detectors/reply-detector.js +82 -0
  19. package/dist/detectors/types.d.ts +38 -0
  20. package/dist/detectors/types.js +2 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.js +132 -0
  23. package/dist/inline-layer.d.ts +7 -0
  24. package/dist/inline-layer.js +116 -0
  25. package/dist/mime-layer.d.ts +15 -0
  26. package/dist/mime-layer.js +70 -0
  27. package/dist/types.d.ts +63 -0
  28. package/dist/types.js +2 -0
  29. package/dist/utils/cleaner.d.ts +16 -0
  30. package/dist/utils/cleaner.js +51 -0
  31. package/dist/utils.d.ts +17 -0
  32. package/dist/utils.js +221 -0
  33. package/docs/TEST_COVERAGE.md +54 -0
  34. package/docs/architecture/README.md +27 -0
  35. package/docs/architecture/phase1_cc_fix.md +223 -0
  36. package/docs/architecture/phase2_plugin_foundation.md +185 -0
  37. package/docs/architecture/phase3_fallbacks.md +62 -0
  38. package/docs/architecture/plugin_plan.md +318 -0
  39. package/docs/architecture/refactor_report.md +98 -0
  40. package/docs/detectors_usage.md +42 -0
  41. package/docs/walkthrough_address_fix.md +58 -0
  42. package/docs/walkthrough_deep_forward_fix.md +35 -0
  43. package/package.json +48 -0
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutlookEmptyHeaderDetector = void 0;
4
+ const cleaner_1 = require("../utils/cleaner");
5
+ /**
6
+ * Detector for Outlook forwards where the "Envoyé:" (Sent) header is present but empty.
7
+ * Example of failing block:
8
+ * ________________________________
9
+ * De: Florian M.
10
+ * Envoyé:
11
+ * À: Flo M.
12
+ * Objet: RE: ...
13
+ */
14
+ class OutlookEmptyHeaderDetector {
15
+ constructor() {
16
+ this.name = 'outlook_empty_header';
17
+ this.priority = 50; // Fallback for corrupted headers (after specifics, before generic Crisp)
18
+ // Regex to capture the header block:
19
+ // 1. Optional Separator (mostly underscores)
20
+ // 2. De: ... (From)
21
+ // 3. Envoyé: ... (Date) - Allow to be empty
22
+ // 4. À: ... (To)
23
+ // 5. Objet: ... (Subject)
24
+ this.HEADER_PATTERN = /^(?:_{30,}[ \t]*)?[\r\n]*De\s*:[ \t]*([^\r\n]+)\r?\nEnvoy(?:[é|e]|=[E|e]9)(?:[ \t]*:[ \t]*|\s*=\s*E9\s*:[ \t]*)(.*)\r?\n(?:[ÀA]|\=[C|c]0)\s*:[ \t]*([^\r\n]+)\r?\nObjet\s*:[ \t]*([^\r\n]+)/im;
25
+ }
26
+ detect(text) {
27
+ // 1. Expert Normalization
28
+ const normalized = cleaner_1.Cleaner.normalize(text);
29
+ const match = this.HEADER_PATTERN.exec(normalized);
30
+ if (match) {
31
+ const fullMatch = match[0];
32
+ const fromLine = match[1].trim();
33
+ const dateLine = match[2].trim();
34
+ const toLine = match[3].trim();
35
+ const subjectLine = match[4].trim();
36
+ const matchIndex = normalized.indexOf(fullMatch);
37
+ const message = normalized.substring(0, matchIndex).trim();
38
+ // 2. Expert Body Extraction
39
+ const lines = normalized.split('\n');
40
+ // Find line index of the end of the header match
41
+ const textUntilEnd = normalized.substring(0, matchIndex + fullMatch.length);
42
+ const lastHeaderLineIndex = textUntilEnd.split('\n').length - 1;
43
+ const bodyContent = cleaner_1.Cleaner.extractBody(lines, lastHeaderLineIndex);
44
+ // If the block started with a quote, we must strip quotes
45
+ const finalBody = fullMatch.trim().startsWith('>') ? cleaner_1.Cleaner.stripQuotes(bodyContent) : bodyContent;
46
+ if (fromLine.length > 0) {
47
+ return {
48
+ found: true,
49
+ detector: this.name,
50
+ confidence: 'high',
51
+ message: message || undefined,
52
+ email: {
53
+ from: fromLine,
54
+ subject: subjectLine,
55
+ date: dateLine || undefined,
56
+ body: finalBody
57
+ }
58
+ };
59
+ }
60
+ }
61
+ return { found: false, confidence: 'low' };
62
+ }
63
+ }
64
+ exports.OutlookEmptyHeaderDetector = OutlookEmptyHeaderDetector;
@@ -0,0 +1,10 @@
1
+ import { ForwardDetector, DetectionResult } from './types';
2
+ /**
3
+ * Detector for French Outlook format (and variations)
4
+ * Handles "De: / Envoyé: / À: / Objet:" in any order
5
+ */
6
+ export declare class OutlookFRDetector implements ForwardDetector {
7
+ readonly name = "outlook_fr";
8
+ readonly priority = -30;
9
+ detect(text: string): DetectionResult;
10
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutlookFRDetector = void 0;
4
+ const cleaner_1 = require("../utils/cleaner");
5
+ /**
6
+ * Detector for French Outlook format (and variations)
7
+ * Handles "De: / Envoyé: / À: / Objet:" in any order
8
+ */
9
+ class OutlookFRDetector {
10
+ constructor() {
11
+ this.name = 'outlook_fr';
12
+ this.priority = -30; // Specific detector - High Priority (Override)
13
+ }
14
+ detect(text) {
15
+ // 1. Expert Normalization
16
+ const normalized = cleaner_1.Cleaner.normalize(text);
17
+ const lines = normalized.split('\n');
18
+ // Safe patterns: must be start of line
19
+ const dePattern = /^[ \t]*De\s*:/i;
20
+ const objetPattern = /^[ \t]*Objet\s*:/i;
21
+ const envoyePattern = /^[ \t]*Envoy(?:é|=E9|e)?\s*:/i;
22
+ const aPattern = /^[ \t]*(?:À|A|=C0)\s*:/i;
23
+ const datePattern = /^[ \t]*Date\s*:/i;
24
+ // Find the FIRST potential header as an anchor
25
+ let anchorIndex = -1;
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i];
28
+ if (dePattern.test(line) || objetPattern.test(line)) {
29
+ anchorIndex = i;
30
+ break;
31
+ }
32
+ }
33
+ if (anchorIndex === -1)
34
+ return { found: false, confidence: 'low' };
35
+ // Look in a window around the anchor (usually headers are clustered within 10 lines)
36
+ // CRITICAL: The window must stop if we hit an empty line (end of headers)
37
+ let searchWindow = [];
38
+ const windowLimit = 15;
39
+ const searchStart = Math.max(0, anchorIndex - 2);
40
+ for (let i = searchStart; i < Math.min(lines.length, anchorIndex + windowLimit); i++) {
41
+ if (i > anchorIndex && lines[i].trim() === '')
42
+ break;
43
+ searchWindow.push(lines[i]);
44
+ }
45
+ const findInWindow = (pattern) => {
46
+ for (let i = 0; i < searchWindow.length; i++) {
47
+ if (pattern.test(searchWindow[i])) {
48
+ return { index: searchStart + i, line: searchWindow[i] };
49
+ }
50
+ }
51
+ return null;
52
+ };
53
+ const de = findInWindow(dePattern);
54
+ const objet = findInWindow(objetPattern);
55
+ // Required headers for confidence
56
+ if (!de || !objet) {
57
+ // console.log('OutlookFRDetector: Required headers missing in window');
58
+ return { found: false, confidence: 'low' };
59
+ }
60
+ const envoye = findInWindow(envoyePattern);
61
+ const date = findInWindow(datePattern);
62
+ const a = findInWindow(aPattern);
63
+ const foundHeaders = [de, objet];
64
+ if (envoye)
65
+ foundHeaders.push(envoye);
66
+ if (date)
67
+ foundHeaders.push(date);
68
+ if (a)
69
+ foundHeaders.push(a);
70
+ const firstHeaderIndex = Math.min(...foundHeaders.map(h => h.index));
71
+ const lastHeaderIndex = Math.max(...foundHeaders.map(h => h.index));
72
+ // 2. Expert Body Extraction
73
+ const bodyContent = cleaner_1.Cleaner.extractBody(lines, lastHeaderIndex);
74
+ const finalBody = lines[firstHeaderIndex].startsWith('>') ? cleaner_1.Cleaner.stripQuotes(bodyContent) : bodyContent;
75
+ // 3. Extract metadata
76
+ const extractValue = (line) => {
77
+ const colonIdx = line.indexOf(':');
78
+ return colonIdx !== -1 ? line.substring(colonIdx + 1).trim() : '';
79
+ };
80
+ const subject = extractValue(objet.line);
81
+ const dateRaw = envoye ? extractValue(envoye.line) : (date ? extractValue(date.line) : undefined);
82
+ const deValue = extractValue(de.line);
83
+ // Simple name/email split for 'De:'
84
+ const deMatch = deValue.match(/(.+?)(?:\s*[<\[](.+?)[>\]])?\s*$/);
85
+ const fromName = deMatch ? deMatch[1].trim().replace(/["']/g, '') : deValue;
86
+ const fromEmail = deMatch && deMatch[2] ? deMatch[2].trim() : (fromName.includes('@') ? fromName : '');
87
+ // 4. Message (preceding text)
88
+ let messageEnd = firstHeaderIndex;
89
+ if (messageEnd > 0) {
90
+ for (let i = 1; i <= 5; i++) {
91
+ const prevLineIdx = firstHeaderIndex - i;
92
+ if (prevLineIdx < 0)
93
+ break;
94
+ const prevLine = lines[prevLineIdx].trim();
95
+ if (prevLine.match(/^-{2,}.*-{2,}$/) || prevLine.match(/^_{3,}$/)) {
96
+ messageEnd = prevLineIdx;
97
+ break;
98
+ }
99
+ if (prevLine === '')
100
+ continue;
101
+ break;
102
+ }
103
+ }
104
+ return {
105
+ found: true,
106
+ email: {
107
+ from: fromEmail.includes('@')
108
+ ? { name: fromName !== fromEmail ? fromName : '', address: fromEmail }
109
+ : { name: fromName, address: fromName },
110
+ subject,
111
+ date: dateRaw,
112
+ body: finalBody
113
+ },
114
+ message: messageEnd > 0 ? lines.slice(0, messageEnd).join('\n').trim() : undefined,
115
+ confidence: 'high'
116
+ };
117
+ }
118
+ }
119
+ exports.OutlookFRDetector = OutlookFRDetector;
@@ -0,0 +1,13 @@
1
+ import { ForwardDetector, DetectionResult } from './types';
2
+ /**
3
+ * Detector for Outlook forwards (French) where "Envoyé:" comes BEFORE "De:".
4
+ */
5
+ export declare class OutlookReverseFrDetector implements ForwardDetector {
6
+ readonly name = "outlook_reverse_fr";
7
+ readonly priority = -20;
8
+ private readonly ENVOYE_PATTERN;
9
+ private readonly DE_PATTERN;
10
+ private readonly A_PATTERN;
11
+ private readonly OBJET_PATTERN;
12
+ detect(text: string): DetectionResult;
13
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutlookReverseFrDetector = void 0;
4
+ const cleaner_1 = require("../utils/cleaner");
5
+ /**
6
+ * Detector for Outlook forwards (French) where "Envoyé:" comes BEFORE "De:".
7
+ */
8
+ class OutlookReverseFrDetector {
9
+ constructor() {
10
+ this.name = 'outlook_reverse_fr';
11
+ this.priority = -20; // Specific detector - High Priority (Override)
12
+ // Regex patterns for field detection
13
+ this.ENVOYE_PATTERN = /^[ \t]*Envoy(?:é|=E9|e)?\s*:\s*(.*?)\s*$/m;
14
+ this.DE_PATTERN = /^[ \t]*De\s*:/i;
15
+ this.A_PATTERN = /^[ \t]*(?:À|A|=C0)\s*:/i;
16
+ this.OBJET_PATTERN = /^[ \t]*Objet\s*:/i;
17
+ }
18
+ detect(text) {
19
+ // 1. Expert Normalization
20
+ const normalized = cleaner_1.Cleaner.normalize(text);
21
+ const lines = normalized.split('\n');
22
+ // Find "Envoyé:" as an anchor
23
+ const envoyeMatch = this.ENVOYE_PATTERN.exec(normalized);
24
+ if (!envoyeMatch)
25
+ return { found: false, confidence: 'low' };
26
+ const envoyeIdx = envoyeMatch.index;
27
+ // Search in a window after "Envoyé:" for "De:"
28
+ // Combined with a window-stop at empty line
29
+ const windowLimit = 15;
30
+ const textUntilEnvoye = normalized.substring(0, envoyeIdx);
31
+ const envoyeLineIndex = textUntilEnvoye.split('\n').length - 1;
32
+ let searchWindow = [];
33
+ for (let i = envoyeLineIndex; i < Math.min(lines.length, envoyeLineIndex + windowLimit); i++) {
34
+ if (i > envoyeLineIndex && lines[i].trim() === '')
35
+ break;
36
+ searchWindow.push(lines[i]);
37
+ }
38
+ const findInWindow = (pattern) => {
39
+ for (let i = 0; i < searchWindow.length; i++) {
40
+ if (pattern.test(searchWindow[i])) {
41
+ return { index: envoyeLineIndex + i, line: searchWindow[i] };
42
+ }
43
+ }
44
+ return null;
45
+ };
46
+ const de = findInWindow(this.DE_PATTERN);
47
+ if (!de)
48
+ return { found: false, confidence: 'low' };
49
+ const a = findInWindow(this.A_PATTERN);
50
+ const objet = findInWindow(this.OBJET_PATTERN);
51
+ const foundHeaders = [{ index: envoyeLineIndex, line: envoyeMatch[0] }, de];
52
+ if (a)
53
+ foundHeaders.push(a);
54
+ if (objet)
55
+ foundHeaders.push(objet);
56
+ const firstHeaderIndex = Math.min(...foundHeaders.map(h => h.index));
57
+ const lastHeaderIndex = Math.max(...foundHeaders.map(h => h.index));
58
+ // 2. Expert Body Extraction
59
+ const bodyContent = cleaner_1.Cleaner.extractBody(lines, lastHeaderIndex);
60
+ const finalBody = lines[firstHeaderIndex].startsWith('>') ? cleaner_1.Cleaner.stripQuotes(bodyContent) : bodyContent;
61
+ // 3. Metadata
62
+ const extractValue = (line) => {
63
+ const colonIdx = line.indexOf(':');
64
+ return colonIdx !== -1 ? line.substring(colonIdx + 1).trim() : '';
65
+ };
66
+ const deValue = extractValue(de.line);
67
+ const deMatch = deValue.match(/(.+?)(?:\s*[<\[](.+?)[>\]])?\s*$/);
68
+ const fromName = deMatch ? deMatch[1].trim().replace(/["']/g, '') : deValue;
69
+ const fromEmail = deMatch && deMatch[2] ? deMatch[2].trim() : (fromName.includes('@') ? fromName : '');
70
+ return {
71
+ found: true,
72
+ detector: this.name,
73
+ confidence: 'high',
74
+ message: firstHeaderIndex > 0 ? lines.slice(0, firstHeaderIndex).join('\n').trim() : undefined,
75
+ email: {
76
+ from: fromEmail.includes('@')
77
+ ? { name: fromName !== fromEmail ? fromName : '', address: fromEmail }
78
+ : { name: fromName, address: fromName },
79
+ subject: objet ? extractValue(objet.line) : '',
80
+ date: extractValue(envoyeMatch[0]),
81
+ body: finalBody
82
+ }
83
+ };
84
+ }
85
+ }
86
+ exports.OutlookReverseFrDetector = OutlookReverseFrDetector;
@@ -0,0 +1,25 @@
1
+ import { ForwardDetector, DetectionResult } from './types';
2
+ /**
3
+ * Registry for managing forward detection plugins
4
+ * Detectors are tried in priority order (lower number = higher priority)
5
+ */
6
+ export declare class DetectorRegistry {
7
+ private detectors;
8
+ constructor(customDetectors?: ForwardDetector[]);
9
+ /**
10
+ * Register a new detector
11
+ * @param detector The detector to register
12
+ */
13
+ register(detector: ForwardDetector): void;
14
+ /**
15
+ * Attempt to detect a forward using all registered detectors
16
+ * Detectors are tried in priority order until one succeeds
17
+ * @param text The text to analyze
18
+ * @returns Detection result from the first successful detector
19
+ */
20
+ detect(text: string): DetectionResult;
21
+ /**
22
+ * Get all registered detector names in priority order
23
+ */
24
+ getDetectorNames(): string[];
25
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DetectorRegistry = void 0;
4
+ const crisp_detector_1 = require("./crisp-detector");
5
+ const outlook_fr_detector_1 = require("./outlook-fr-detector");
6
+ const new_outlook_detector_1 = require("./new-outlook-detector");
7
+ const outlook_empty_header_detector_1 = require("./outlook-empty-header-detector");
8
+ const outlook_reverse_fr_detector_1 = require("./outlook-reverse-fr-detector");
9
+ const reply_detector_1 = require("./reply-detector");
10
+ /**
11
+ * Registry for managing forward detection plugins
12
+ * Detectors are tried in priority order (lower number = higher priority)
13
+ */
14
+ class DetectorRegistry {
15
+ constructor(customDetectors = []) {
16
+ this.detectors = [];
17
+ // Register all detectors (priority determines order)
18
+ this.register(new crisp_detector_1.CrispDetector()); // priority: 0 (highest - universal library)
19
+ this.register(new outlook_empty_header_detector_1.OutlookEmptyHeaderDetector()); // priority: 5 (handle empty headers)
20
+ this.register(new outlook_reverse_fr_detector_1.OutlookReverseFrDetector()); // priority: 6 (handle reversed FR headers)
21
+ this.register(new reply_detector_1.ReplyDetector()); // priority: 7 (handle standard replies)
22
+ this.register(new outlook_fr_detector_1.OutlookFRDetector()); // priority: 10 (fallback for FR formats)
23
+ this.register(new new_outlook_detector_1.NewOutlookDetector()); // priority: 10 (fallback for new Outlook)
24
+ // Register custom detectors
25
+ customDetectors.forEach(detector => this.register(detector));
26
+ }
27
+ /**
28
+ * Register a new detector
29
+ * @param detector The detector to register
30
+ */
31
+ register(detector) {
32
+ this.detectors.push(detector);
33
+ // Sort by priority (lower number = higher priority)
34
+ this.detectors.sort((a, b) => a.priority - b.priority);
35
+ }
36
+ /**
37
+ * Attempt to detect a forward using all registered detectors
38
+ * Detectors are tried in priority order until one succeeds
39
+ * @param text The text to analyze
40
+ * @returns Detection result from the first successful detector
41
+ */
42
+ detect(text) {
43
+ let bestResult = null;
44
+ let bestIndex = Infinity;
45
+ for (const detector of this.detectors) {
46
+ const result = detector.detect(text);
47
+ if (result.found) {
48
+ // Check if the result is actually useful (has an email address)
49
+ const hasValidEmail = result.email && ((typeof result.email.from === 'string' && result.email.from.trim().length > 0) ||
50
+ (typeof result.email.from === 'object' && (result.email.from.address?.trim() || result.email.from.name?.trim())));
51
+ if (hasValidEmail) {
52
+ // Calculate the position of the match based on the length of the preceding message
53
+ // We assume result.message is the text BEFORE the forward.
54
+ const matchIndex = result.message ? result.message.length : 0;
55
+ // If this match is earlier in the text, it's a better candidate for the "next" forward
56
+ // If matches start at the same position, fallback to priority (order in this.detectors)
57
+ if (matchIndex < bestIndex) {
58
+ bestIndex = matchIndex;
59
+ result.detector = detector.name;
60
+ bestResult = result;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ if (bestResult) {
66
+ return bestResult;
67
+ }
68
+ // No detector found a forward
69
+ return {
70
+ found: false,
71
+ confidence: 'low'
72
+ };
73
+ }
74
+ /**
75
+ * Get all registered detector names in priority order
76
+ */
77
+ getDetectorNames() {
78
+ return this.detectors.map(d => d.name);
79
+ }
80
+ }
81
+ exports.DetectorRegistry = DetectorRegistry;
@@ -0,0 +1,11 @@
1
+ import { ForwardDetector, DetectionResult } from './types';
2
+ /**
3
+ * Reply detector - matches "On [date], [user] wrote:" and its localized variations
4
+ * These are common in reply headers (Apple Mail, Outlook, etc.)
5
+ */
6
+ export declare class ReplyDetector implements ForwardDetector {
7
+ readonly name = "reply";
8
+ readonly priority = 150;
9
+ private patterns;
10
+ detect(text: string): DetectionResult;
11
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReplyDetector = void 0;
4
+ const cleaner_1 = require("../utils/cleaner");
5
+ /**
6
+ * Reply detector - matches "On [date], [user] wrote:" and its localized variations
7
+ * These are common in reply headers (Apple Mail, Outlook, etc.)
8
+ */
9
+ class ReplyDetector {
10
+ constructor() {
11
+ this.name = 'reply';
12
+ this.priority = 150;
13
+ // Localized patterns based on email-forward-parser's separator_with_information
14
+ this.patterns = [
15
+ /^\s*>?\s*Dne\s+(?<date>.+)\,\s+(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+napsal\(a\)\s*:/mi,
16
+ /^\s*>?\s*D.\s+(?<date>.+)\s+skrev\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]? ?: ?/mi,
17
+ /^\s*>?\s*Am\s+(?<date>.+)\s+schrieb\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]? ?: ?/mi,
18
+ /^\s*>?\s*On\s+(?<date>.+)\,\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+wrote ?: ?/mi,
19
+ /^\s*>?\s*On\s+(?<date>.+)\s+at\s+(?<time>.+)\,\s+(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+wrote ?: ?/mi,
20
+ /^\s*>?\s*On\s+(?<date>.+)\,\s+(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+wrote ?: ?/mi,
21
+ /^\s*>?\s*On\s+(?<date>.+)\s+(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+wrote ?: ?/mi,
22
+ /^\s*>?\s*El\s+(?<date>.+)\,\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+escribió ?: ?/mi,
23
+ /^\s*>?\s*Le\s+(?<date>.+)\,\s+[«"]?(?<from_name>.+)[»"]?\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+a écrit ?: ?/mi,
24
+ /^\s*>?\s*(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+kirjoitti\s+(?<date>.+) ?: ?/mi,
25
+ /^\s*>?\s*(?<date>.+)\s+időpontban\s+(?<from_name>.+)\s*[\[|<|(]?(?<from_address>.+)?[\]|>|)]?\s+ezt írta ?: ?/mi,
26
+ /^\s*>?\s*Il giorno\s+(?<date>.+)\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+ha scritto ?: ?/mi,
27
+ /^\s*>?\s*Op\s+(?<date>.+)\s+heeft\s+(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+geschreven ?: ?/mi,
28
+ /^\s*>?\s*(?<from_name>.+)\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+skrev følgende den\s+(?<date>.+) ?: ?/mi,
29
+ /^\s*>?\s*Dnia\s+(?<date>.+)\s+[„"]?(?<from_name>.+)[”"]?\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+napisał ?: ?/mi,
30
+ /^\s*>?\s*Em\s+(?<date>.+)\,\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+escreveu ?: ?/mi,
31
+ /^\s*>?\s*(?<date>.+)\s+пользователь\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+написал ?: ?/mi,
32
+ /^\s*>?\s*(?<date>.+)\s+používateľ\s+(?<from_name>.+)\s*\([\[|<]?(?<from_address>.+)?[\]|>]\)?\s+napísal ?: ?/mi,
33
+ /^\s*>?\s*Den\s+(?<date>.+)\s+skrev\s+"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\s+följande ?: ?/mi,
34
+ /^\s*>?\s*"(?<from_name>.+)"\s*[\[|<]?(?<from_address>.+)?[\]|>]?\,\s+(?<date>.+)\s+tarihinde şunu yazdı ?: ?/mi
35
+ ];
36
+ }
37
+ detect(text) {
38
+ // 1. Expert Normalization
39
+ const normalized = cleaner_1.Cleaner.normalize(text);
40
+ const lines = normalized.split('\n');
41
+ // Search in the first 30 lines
42
+ for (let i = 0; i < Math.min(lines.length, 30); i++) {
43
+ const line = lines[i].trim();
44
+ if (!line)
45
+ continue;
46
+ const nextLine = (i + 1 < lines.length) ? lines[i + 1].trim() : '';
47
+ const combinedLine = `${line} ${nextLine}`.trim();
48
+ for (const pattern of this.patterns) {
49
+ let match = line.match(pattern);
50
+ let isMultiLine = false;
51
+ if (!match) {
52
+ match = combinedLine.match(pattern);
53
+ isMultiLine = !!match;
54
+ }
55
+ if (match && match.groups) {
56
+ const { from_name, from_address, date, time } = match.groups;
57
+ const fullDate = time ? `${date} ${time}` : date;
58
+ const lastHeaderIndex = isMultiLine ? i + 1 : i;
59
+ // 2. Expert Body Extraction
60
+ const bodyContent = cleaner_1.Cleaner.extractBody(lines, lastHeaderIndex);
61
+ const finalBody = line.startsWith('>') ? cleaner_1.Cleaner.stripQuotes(bodyContent) : bodyContent;
62
+ return {
63
+ found: true,
64
+ detector: this.name,
65
+ email: {
66
+ from: {
67
+ name: from_name?.trim() || '',
68
+ address: from_address?.trim() || ''
69
+ },
70
+ date: fullDate?.trim(),
71
+ body: finalBody
72
+ },
73
+ message: i > 0 ? lines.slice(0, i).join('\n').trim() : undefined,
74
+ confidence: 'medium'
75
+ };
76
+ }
77
+ }
78
+ }
79
+ return { found: false, confidence: 'low' };
80
+ }
81
+ }
82
+ exports.ReplyDetector = ReplyDetector;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Result of a forward detection attempt
3
+ */
4
+ export interface DetectionResult {
5
+ /** Whether a forward was detected */
6
+ found: boolean;
7
+ /** Extracted email data if found */
8
+ email?: {
9
+ from: string | {
10
+ name: string;
11
+ address: string;
12
+ };
13
+ subject?: string;
14
+ date?: string;
15
+ body?: string;
16
+ };
17
+ /** Exclusive content before the forward separator */
18
+ message?: string;
19
+ /** Identifier of the successful detector */
20
+ detector?: string;
21
+ /** Confidence level of the detection */
22
+ confidence: 'high' | 'medium' | 'low';
23
+ }
24
+ /**
25
+ * Interface for forward detection plugins
26
+ */
27
+ export interface ForwardDetector {
28
+ /** Unique identifier for this detector */
29
+ readonly name: string;
30
+ /** Priority (lower number = higher priority, Crisp = 0) */
31
+ readonly priority: number;
32
+ /**
33
+ * Attempt to detect a forwarded email in the given text
34
+ * @param text The text to analyze
35
+ * @returns Detection result with email data if found
36
+ */
37
+ detect(text: string): DetectionResult;
38
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { Options, ResultObject } from './types';
2
+ /**
3
+ * Main entry point: Extract the deepest forwarded email using hybrid strategy
4
+ */
5
+ export declare function extractDeepestHybrid(raw: string, options?: Options): Promise<ResultObject>;
6
+ export * from './types';