fix: strip reply/forward prefixes followed by a full-width colon
The prefix-stripping regex only matched an ASCII ":", so a localized prefix from a CJK mail client (e.g. "回复:foo", using the full-width colon U+FF1A) was left in place. On reply this caused the user's own prefix to be stacked on top, growing the subject chain. Accept both ":" and ":" after the prefix token. Adds tests.
This commit is contained in:
committed by
Linus Rath
parent
751f3c1685
commit
8af6694152
@@ -10,10 +10,9 @@ describe('stripSubjectPrefixes', () => {
|
|||||||
expect(stripSubjectPrefixes('Re: AW: WG: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Re: AW: WG: foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('strips the Outlook [N] counter and Eudora *N counter', () => {
|
it('strips the Outlook [N] and Eudora *N counters', () => {
|
||||||
expect(stripSubjectPrefixes('Re[2]: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Re[2]: foo')).toBe('foo');
|
||||||
expect(stripSubjectPrefixes('Re*3: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Re*3: foo')).toBe('foo');
|
||||||
expect(stripSubjectPrefixes('Re*: foo')).toBe('foo');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is case-insensitive and idempotent', () => {
|
it('is case-insensitive and idempotent', () => {
|
||||||
@@ -21,47 +20,37 @@ describe('stripSubjectPrefixes', () => {
|
|||||||
expect(stripSubjectPrefixes(stripSubjectPrefixes('RE: Re: foo'))).toBe('foo');
|
expect(stripSubjectPrefixes(stripSubjectPrefixes('RE: Re: foo'))).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('strips a Cyrillic reply token', () => {
|
it('strips a Cyrillic token and an ASCII-colon Chinese token', () => {
|
||||||
expect(stripSubjectPrefixes('Ответ: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Ответ: foo')).toBe('foo');
|
||||||
});
|
|
||||||
|
|
||||||
it('strips a Chinese token followed by an ASCII colon', () => {
|
|
||||||
expect(stripSubjectPrefixes('回复: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('回复: foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CHARACTERISATION: does NOT strip a token followed by a full-width colon', () => {
|
it('strips a token followed by a full-width colon (CJK clients)', () => {
|
||||||
// The colon in the regex is ASCII ":"; a full-width ":" (U+FF1A), as some
|
expect(stripSubjectPrefixes('回复:foo')).toBe('foo');
|
||||||
// CJK mail clients emit, is left untouched. Likely a bug — see follow-ups.
|
expect(stripSubjectPrefixes('回覆:foo')).toBe('foo');
|
||||||
expect(stripSubjectPrefixes('回复:foo')).toBe('回复:foo');
|
expect(stripSubjectPrefixes('Re:foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does NOT strip a bare single-letter "R:" (would eat real subjects)', () => {
|
it('still does not strip a bare single-letter "R:"', () => {
|
||||||
expect(stripSubjectPrefixes('R: budget 2024')).toBe('R: budget 2024');
|
expect(stripSubjectPrefixes('R: budget 2024')).toBe('R: budget 2024');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns "" for empty / null / undefined', () => {
|
it('returns "" for empty / null / undefined and leaves clean subjects alone', () => {
|
||||||
expect(stripSubjectPrefixes('')).toBe('');
|
expect(stripSubjectPrefixes('')).toBe('');
|
||||||
expect(stripSubjectPrefixes(null)).toBe('');
|
expect(stripSubjectPrefixes(null)).toBe('');
|
||||||
expect(stripSubjectPrefixes(undefined)).toBe('');
|
expect(stripSubjectPrefixes(undefined)).toBe('');
|
||||||
});
|
|
||||||
|
|
||||||
it('leaves a prefix-free subject untouched', () => {
|
|
||||||
expect(stripSubjectPrefixes('foo')).toBe('foo');
|
expect(stripSubjectPrefixes('foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildReplySubject / buildForwardSubject', () => {
|
describe('buildReplySubject / buildForwardSubject', () => {
|
||||||
it('replaces an existing prefix chain with the given prefix', () => {
|
it('replaces a prefix chain (incl. a full-width colon) with the given prefix', () => {
|
||||||
expect(buildReplySubject('AW: WG: foo', 'Re:')).toBe('Re: foo');
|
expect(buildReplySubject('回复:foo', 'Re:')).toBe('Re: foo');
|
||||||
expect(buildForwardSubject('Re: foo', 'Fwd:')).toBe('Fwd: foo');
|
expect(buildForwardSubject('Re: foo', 'Fwd:')).toBe('Fwd: foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prepends the prefix to a prefix-free subject', () => {
|
it('prepends to a clean subject and returns the bare prefix for empty input', () => {
|
||||||
expect(buildReplySubject('foo', 'AW:')).toBe('AW: foo');
|
expect(buildReplySubject('foo', 'AW:')).toBe('AW: foo');
|
||||||
});
|
|
||||||
|
|
||||||
it('returns just the bare prefix for an empty subject', () => {
|
|
||||||
expect(buildReplySubject('', 'AW:')).toBe('AW:');
|
expect(buildReplySubject('', 'AW:')).toBe('AW:');
|
||||||
expect(buildForwardSubject(null, 'Fwd:')).toBe('Fwd:');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ function buildPrefixRegex(tokens: string[]): RegExp {
|
|||||||
// Sort by length DESC so longer tokens (e.g. "Пересл") win over their
|
// Sort by length DESC so longer tokens (e.g. "Пересл") win over their
|
||||||
// shorter prefixes (e.g. "Пер") during alternation matching.
|
// shorter prefixes (e.g. "Пер") during alternation matching.
|
||||||
escaped.sort((a, b) => b.length - a.length);
|
escaped.sort((a, b) => b.length - a.length);
|
||||||
|
// Accept both the ASCII colon and the full-width colon ":" (U+FF1A) that CJK
|
||||||
|
// mail clients emit after a localized prefix (e.g. "回复:foo").
|
||||||
return new RegExp(
|
return new RegExp(
|
||||||
`^\\s*(?:${escaped.join("|")})(?:\\[\\d+\\]|\\*\\d*)?\\s*:\\s*`,
|
`^\\s*(?:${escaped.join("|")})(?:\\[\\d+\\]|\\*\\d*)?\\s*[:\\uFF1A]\\s*`,
|
||||||
"i",
|
"i",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user