307 lines
8.3 KiB
Dart
307 lines
8.3 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_parser.dart';
|
|
|
|
SieveEmailContext _email({
|
|
String subject = '',
|
|
String from = '',
|
|
String to = '',
|
|
int size = 0,
|
|
}) {
|
|
return SieveEmailContext(
|
|
headers: {
|
|
if (subject.isNotEmpty) 'subject': [subject],
|
|
if (from.isNotEmpty) 'from': [from],
|
|
if (to.isNotEmpty) 'to': [to],
|
|
},
|
|
sizeBytes: size,
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
final parser = SieveParser();
|
|
final interp = SieveInterpreter();
|
|
|
|
SieveExecutionContext run(String script, SieveEmailContext email) {
|
|
return interp.execute(parser.parse(script), email);
|
|
}
|
|
|
|
group('SieveParser — require', () {
|
|
test('require is parsed without error', () {
|
|
expect(
|
|
() => parser.parse('require ["fileinto", "imap4flags"];'),
|
|
returnsNormally,
|
|
);
|
|
});
|
|
|
|
test('require produces no rules', () {
|
|
final rules = parser.parse('require ["fileinto"];');
|
|
expect(rules, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — basic if/discard', () {
|
|
test('discard on subject :contains', () {
|
|
const script = '''
|
|
require ["fileinto"];
|
|
if header :contains "Subject" "Spam" {
|
|
discard;
|
|
}
|
|
''';
|
|
var ctx = run(script, _email(subject: 'This is Spam!'));
|
|
expect(ctx.isCancelled, isTrue);
|
|
|
|
ctx = run(script, _email(subject: 'Hello'));
|
|
expect(ctx.isCancelled, isFalse);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — fileinto + flag', () {
|
|
test('flag and fileinto on :is match across multiple headers', () {
|
|
const script = '''
|
|
require ["fileinto", "imap4flags"];
|
|
if header :is ["From", "Reply-To"] "boss@work.com" {
|
|
flag ["\\\\Important"];
|
|
fileinto "Work";
|
|
}
|
|
''';
|
|
final ctx = run(script, _email(from: 'boss@work.com'));
|
|
expect(ctx.targetFolders, contains('Work'));
|
|
expect(ctx.keepInInbox, isFalse);
|
|
});
|
|
|
|
test('no match means inbox is kept', () {
|
|
const script = '''
|
|
if header :is "From" "boss@work.com" {
|
|
fileinto "Work";
|
|
}
|
|
''';
|
|
final ctx = run(script, _email(from: 'other@example.com'));
|
|
expect(ctx.targetFolders, isEmpty);
|
|
expect(ctx.keepInInbox, isTrue);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — anyof', () {
|
|
test('anyof fires when any sub-condition matches', () {
|
|
const script = '''
|
|
if anyof (
|
|
header :contains "Subject" "Newsletter",
|
|
header :contains "Subject" "Promotion"
|
|
) {
|
|
fileinto "Bulk";
|
|
}
|
|
''';
|
|
var ctx = run(script, _email(subject: 'Weekly Newsletter'));
|
|
expect(ctx.targetFolders, contains('Bulk'));
|
|
|
|
ctx = run(script, _email(subject: 'Big Promotion Inside'));
|
|
expect(ctx.targetFolders, contains('Bulk'));
|
|
|
|
ctx = run(script, _email(subject: 'Normal message'));
|
|
expect(ctx.targetFolders, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — allof', () {
|
|
test('allof fires only when all conditions match', () {
|
|
const script = '''
|
|
if allof (
|
|
header :contains "From" "shop.com",
|
|
header :contains "Subject" "deal"
|
|
) {
|
|
fileinto "Deals";
|
|
}
|
|
''';
|
|
var ctx = run(
|
|
script,
|
|
_email(from: 'offers@shop.com', subject: 'Hot deal today'),
|
|
);
|
|
expect(ctx.targetFolders, contains('Deals'));
|
|
|
|
ctx = run(
|
|
script,
|
|
_email(from: 'friend@example.com', subject: 'Hot deal today'),
|
|
);
|
|
expect(ctx.targetFolders, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — size condition', () {
|
|
test(':over with K suffix', () {
|
|
const script = '''
|
|
if size :over 100K {
|
|
fileinto "Large";
|
|
}
|
|
''';
|
|
var ctx = run(script, _email(size: 200 * 1024));
|
|
expect(ctx.targetFolders, contains('Large'));
|
|
|
|
ctx = run(script, _email(size: 50 * 1024));
|
|
expect(ctx.targetFolders, isEmpty);
|
|
});
|
|
|
|
test(':under fires for small messages', () {
|
|
const script = '''
|
|
if size :under 1K {
|
|
fileinto "Tiny";
|
|
}
|
|
''';
|
|
final ctx = run(script, _email(size: 500));
|
|
expect(ctx.targetFolders, contains('Tiny'));
|
|
});
|
|
});
|
|
|
|
group('SieveParser — if/elsif/else', () {
|
|
const script = '''
|
|
if header :contains "Subject" "Spam" {
|
|
discard;
|
|
} elsif header :contains "Subject" "Work" {
|
|
fileinto "Work";
|
|
} else {
|
|
keep;
|
|
}
|
|
''';
|
|
|
|
test('if branch fires', () {
|
|
final ctx = run(script, _email(subject: 'Spam message'));
|
|
expect(ctx.isCancelled, isTrue);
|
|
});
|
|
|
|
test('elsif branch fires when if does not match', () {
|
|
final ctx = run(script, _email(subject: 'Work update'));
|
|
expect(ctx.isCancelled, isFalse);
|
|
expect(ctx.targetFolders, contains('Work'));
|
|
});
|
|
|
|
test('else branch fires when no condition matched', () {
|
|
final ctx = run(script, _email(subject: 'Hello'));
|
|
expect(ctx.isCancelled, isFalse);
|
|
expect(ctx.targetFolders, isEmpty);
|
|
expect(ctx.keepInInbox, isTrue);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — multiple independent if blocks', () {
|
|
test('both fire independently', () {
|
|
const script = '''
|
|
if header :contains "Subject" "invoice" {
|
|
fileinto "Finance";
|
|
}
|
|
if header :contains "From" "boss@" {
|
|
flag ["\\\\Important"];
|
|
}
|
|
''';
|
|
final ctx = run(
|
|
script,
|
|
_email(subject: 'Invoice #42', from: 'boss@corp.com'),
|
|
);
|
|
expect(ctx.targetFolders, contains('Finance'));
|
|
expect(ctx.flagsToAdd, contains(r'\Important'));
|
|
});
|
|
});
|
|
|
|
group('SieveParser — keep and stop', () {
|
|
test('keep action keeps inbox', () {
|
|
const script = 'keep;';
|
|
final ctx = run(script, _email());
|
|
expect(ctx.keepInInbox, isTrue);
|
|
expect(ctx.isCancelled, isFalse);
|
|
});
|
|
|
|
test('stop acts as implicit keep', () {
|
|
const script = 'stop;';
|
|
final ctx = run(script, _email());
|
|
expect(ctx.keepInInbox, isTrue);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — comments', () {
|
|
test('line comments are ignored', () {
|
|
const script = '''
|
|
# This is a comment
|
|
if header :contains "Subject" "Spam" {
|
|
discard; # inline comment
|
|
}
|
|
''';
|
|
final ctx = run(script, _email(subject: 'Spam'));
|
|
expect(ctx.isCancelled, isTrue);
|
|
});
|
|
|
|
test('block comments are ignored', () {
|
|
const script = '''
|
|
/* block comment */
|
|
if /* inline block */ header :contains "Subject" "Spam" {
|
|
discard;
|
|
}
|
|
''';
|
|
final ctx = run(script, _email(subject: 'Spam'));
|
|
expect(ctx.isCancelled, isTrue);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — exists test', () {
|
|
test('exists fires when header is present and non-empty', () {
|
|
const script = '''
|
|
if exists "X-Spam-Flag" {
|
|
discard;
|
|
}
|
|
''';
|
|
const email = SieveEmailContext(
|
|
headers: {
|
|
'x-spam-flag': ['YES'],
|
|
},
|
|
);
|
|
final ctx = interp.execute(parser.parse(script), email);
|
|
expect(ctx.isCancelled, isTrue);
|
|
});
|
|
});
|
|
|
|
group('SieveParser — rule model', () {
|
|
test('simple if produces one rule with branchGroupId', () {
|
|
final rules = parser.parse(
|
|
'if header :contains "Subject" "x" { discard; }',
|
|
);
|
|
expect(rules, hasLength(1));
|
|
expect(rules.first.branchGroupId, isNotNull);
|
|
expect(rules.first.conditions, hasLength(1));
|
|
expect(rules.first.actions, hasLength(1));
|
|
expect(rules.first.actions.first, isA<DiscardAction>());
|
|
});
|
|
|
|
test('if/elsif produces two rules with the same branchGroupId', () {
|
|
const script = '''
|
|
if header :contains "Subject" "a" { discard; }
|
|
elsif header :contains "Subject" "b" { keep; }
|
|
''';
|
|
final rules = parser.parse(script);
|
|
expect(rules, hasLength(2));
|
|
expect(rules[0].branchGroupId, equals(rules[1].branchGroupId));
|
|
expect(rules[0].isElseBranch, isFalse);
|
|
expect(rules[1].isElseBranch, isFalse);
|
|
});
|
|
|
|
test('else rule has isElseBranch=true', () {
|
|
const script = '''
|
|
if header :contains "Subject" "a" { discard; }
|
|
else { keep; }
|
|
''';
|
|
final rules = parser.parse(script);
|
|
expect(rules, hasLength(2));
|
|
expect(rules.last.isElseBranch, isTrue);
|
|
});
|
|
|
|
test('HeaderCondition fields are populated', () {
|
|
final rules = parser.parse(
|
|
'if header :contains ["From","Reply-To"] "x@y.com" { keep; }',
|
|
);
|
|
final cond = rules.first.conditions.first as HeaderCondition;
|
|
expect(cond.headers, containsAll(['From', 'Reply-To']));
|
|
expect(cond.matchType, ':contains');
|
|
expect(cond.keyList, contains('x@y.com'));
|
|
});
|
|
});
|
|
}
|