344 lines
9.4 KiB
Dart
344 lines
9.4 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|||
|
|
import 'package:sharedinbox/core/sieve/sieve_actions.dart';
|
||
|
|
import 'package:sharedinbox/core/sieve/sieve_conditions.dart';
|
||
|
|
import 'package:sharedinbox/core/sieve/sieve_interpreter.dart';
|
||
|
|
import 'package:sharedinbox/core/sieve/sieve_rule.dart';
|
||
|
|
|
||
|
|
SieveEmailContext _email({
|
||
|
|
String subject = '',
|
||
|
|
String from = '',
|
||
|
|
String to = '',
|
||
|
|
int size = 0,
|
||
|
|
Map<String, List<String>> extra = const {},
|
||
|
|
}) {
|
||
|
|
return SieveEmailContext(
|
||
|
|
headers: {
|
||
|
|
if (subject.isNotEmpty) 'subject': [subject],
|
||
|
|
if (from.isNotEmpty) 'from': [from],
|
||
|
|
if (to.isNotEmpty) 'to': [to],
|
||
|
|
...extra,
|
||
|
|
},
|
||
|
|
sizeBytes: size,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
final interp = SieveInterpreter();
|
||
|
|
|
||
|
|
group('SieveInterpreter — no rules', () {
|
||
|
|
test('empty rule list keeps inbox', () {
|
||
|
|
final ctx = interp.execute([], _email());
|
||
|
|
expect(ctx.keepInInbox, isTrue);
|
||
|
|
expect(ctx.isCancelled, isFalse);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('HeaderCondition :contains', () {
|
||
|
|
test('matches subject substring (case-insensitive)', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['spam']),
|
||
|
|
],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'This is SPAM!'));
|
||
|
|
expect(ctx.isCancelled, isTrue);
|
||
|
|
expect(ctx.keepInInbox, isFalse);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('does not match unrelated subject', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['spam']),
|
||
|
|
],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'Meeting tomorrow'));
|
||
|
|
expect(ctx.isCancelled, isFalse);
|
||
|
|
expect(ctx.keepInInbox, isTrue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('HeaderCondition :is', () {
|
||
|
|
test('exact match on from header', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(
|
||
|
|
['from', 'reply-to'],
|
||
|
|
':is',
|
||
|
|
['boss@work.com'],
|
||
|
|
),
|
||
|
|
],
|
||
|
|
actions: [
|
||
|
|
FlagAction([r'\Important']),
|
||
|
|
FileIntoAction('Work'),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(rules, _email(from: 'boss@work.com'));
|
||
|
|
expect(ctx.flagsToAdd, contains(r'\Important'));
|
||
|
|
expect(ctx.targetFolders, contains('Work'));
|
||
|
|
expect(ctx.keepInInbox, isFalse);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('does not match partial from address', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['from'], ':is', ['boss@work.com']),
|
||
|
|
],
|
||
|
|
actions: [FileIntoAction('Work')],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(rules, _email(from: 'other-boss@work.com'));
|
||
|
|
expect(ctx.targetFolders, isEmpty);
|
||
|
|
expect(ctx.keepInInbox, isTrue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('HeaderCondition :matches (glob)', () {
|
||
|
|
test('* matches any substring', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':matches', ['*newsletter*']),
|
||
|
|
],
|
||
|
|
actions: [FileIntoAction('Bulk')],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx =
|
||
|
|
interp.execute(rules, _email(subject: 'Weekly Newsletter Issue'));
|
||
|
|
expect(ctx.targetFolders, contains('Bulk'));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('SizeCondition', () {
|
||
|
|
test(':over threshold fires', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [SizeCondition(':over', 1024)],
|
||
|
|
actions: [FileIntoAction('Large')],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
final ctx = interp.execute(rules, _email(size: 2048));
|
||
|
|
expect(ctx.targetFolders, contains('Large'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test(':under threshold fires', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [SizeCondition(':under', 500)],
|
||
|
|
actions: [FileIntoAction('Small')],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
final ctx = interp.execute(rules, _email(size: 100));
|
||
|
|
expect(ctx.targetFolders, contains('Small'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test(':over threshold does not fire when size equals threshold', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [SizeCondition(':over', 1024)],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
final ctx = interp.execute(rules, _email(size: 1024));
|
||
|
|
expect(ctx.isCancelled, isFalse);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('allof / anyof join types', () {
|
||
|
|
test('allof fires only when all conditions match', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'allof',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['deal']),
|
||
|
|
HeaderCondition(['from'], ':contains', ['shop.com']),
|
||
|
|
],
|
||
|
|
actions: [FileIntoAction('Deals')],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
// Both match.
|
||
|
|
var ctx = interp.execute(
|
||
|
|
rules,
|
||
|
|
_email(subject: 'Big deal today', from: 'offers@shop.com'),
|
||
|
|
);
|
||
|
|
expect(ctx.targetFolders, contains('Deals'));
|
||
|
|
|
||
|
|
// Only subject matches.
|
||
|
|
ctx = interp.execute(
|
||
|
|
rules,
|
||
|
|
_email(subject: 'Big deal today', from: 'friend@example.com'),
|
||
|
|
);
|
||
|
|
expect(ctx.targetFolders, isEmpty);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('anyof fires when any condition matches', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'anyof',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['spam']),
|
||
|
|
HeaderCondition(['subject'], ':contains', ['advertisement']),
|
||
|
|
],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'Huge advertisement!'));
|
||
|
|
expect(ctx.isCancelled, isTrue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('discard stops processing', () {
|
||
|
|
test('rules after discard are skipped', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['spam']),
|
||
|
|
],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
),
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: const [],
|
||
|
|
actions: [FileIntoAction('Inbox')],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'Spam'));
|
||
|
|
expect(ctx.isCancelled, isTrue);
|
||
|
|
expect(ctx.targetFolders, isEmpty);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('MarkAsSeenAction', () {
|
||
|
|
test('adds \\Seen flag', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: const [],
|
||
|
|
actions: [MarkAsSeenAction()],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
final ctx = interp.execute(rules, _email());
|
||
|
|
expect(ctx.flagsToAdd, contains(r'\Seen'));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('if/elsif/else branch groups', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['spam']),
|
||
|
|
],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
branchGroupId: 1,
|
||
|
|
),
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['work']),
|
||
|
|
],
|
||
|
|
actions: [FileIntoAction('Work')],
|
||
|
|
branchGroupId: 1,
|
||
|
|
),
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: const [],
|
||
|
|
actions: [KeepAction()],
|
||
|
|
branchGroupId: 1,
|
||
|
|
isElseBranch: true,
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
test('first branch fires, subsequent branches are skipped', () {
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'spam message'));
|
||
|
|
expect(ctx.isCancelled, isTrue);
|
||
|
|
expect(ctx.targetFolders, isEmpty);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('second branch fires when first does not match', () {
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'Work update'));
|
||
|
|
expect(ctx.isCancelled, isFalse);
|
||
|
|
expect(ctx.targetFolders, contains('Work'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('else branch fires when no branch matched', () {
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'Hello'));
|
||
|
|
expect(ctx.isCancelled, isFalse);
|
||
|
|
expect(ctx.targetFolders, isEmpty);
|
||
|
|
expect(ctx.keepInInbox, isTrue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('multiple independent rules', () {
|
||
|
|
test('both rules fire when conditions match', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['invoice']),
|
||
|
|
],
|
||
|
|
actions: [FileIntoAction('Finance')],
|
||
|
|
),
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['from'], ':contains', ['boss@']),
|
||
|
|
],
|
||
|
|
actions: [
|
||
|
|
FlagAction([r'\Important']),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
final ctx = interp.execute(
|
||
|
|
rules,
|
||
|
|
_email(subject: 'Invoice #123', from: 'boss@corp.com'),
|
||
|
|
);
|
||
|
|
expect(ctx.targetFolders, contains('Finance'));
|
||
|
|
expect(ctx.flagsToAdd, contains(r'\Important'));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('implicit keep', () {
|
||
|
|
test('keepInInbox stays true when no action changes routing', () {
|
||
|
|
final rules = [
|
||
|
|
SieveRule(
|
||
|
|
joinType: 'single',
|
||
|
|
conditions: [
|
||
|
|
HeaderCondition(['subject'], ':contains', ['nope']),
|
||
|
|
],
|
||
|
|
actions: [DiscardAction()],
|
||
|
|
),
|
||
|
|
];
|
||
|
|
final ctx = interp.execute(rules, _email(subject: 'Hi there'));
|
||
|
|
expect(ctx.keepInInbox, isTrue);
|
||
|
|
expect(ctx.isCancelled, isFalse);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|