137 lines
4.1 KiB
Dart
137 lines
4.1 KiB
Dart
import 'package:sharedinbox/core/sieve/sieve_actions.dart';
|
|
import 'package:sharedinbox/core/sieve/sieve_conditions.dart';
|
|
import 'package:sharedinbox/core/sieve/sieve_rule.dart';
|
|
|
|
/// A lightweight email representation used by [SieveInterpreter].
|
|
/// Header names are lower-cased.
|
|
class SieveEmailContext {
|
|
const SieveEmailContext({required this.headers, this.sizeBytes = 0});
|
|
|
|
final Map<String, List<String>> headers;
|
|
final int sizeBytes;
|
|
|
|
List<String> getHeader(String name) =>
|
|
headers[name.toLowerCase()] ?? const [];
|
|
}
|
|
|
|
/// Tracks the outcome of running a Sieve script against one email.
|
|
class SieveExecutionContext {
|
|
bool isCancelled = false;
|
|
Set<String> targetFolders = {};
|
|
Set<String> flagsToAdd = {};
|
|
bool keepInInbox = true;
|
|
}
|
|
|
|
/// Evaluates a compiled list of [SieveRule]s against a [SieveEmailContext].
|
|
class SieveInterpreter {
|
|
/// Executes [rules] and returns the resulting [SieveExecutionContext].
|
|
///
|
|
/// Rules produced by [SieveParser] may carry a [SieveRule.branchGroupId]
|
|
/// to represent if/elsif/else chains; at most one branch per group fires.
|
|
SieveExecutionContext execute(
|
|
List<SieveRule> rules,
|
|
SieveEmailContext email,
|
|
) {
|
|
final ctx = SieveExecutionContext();
|
|
final firedGroups = <int>{};
|
|
|
|
for (final rule in rules) {
|
|
if (ctx.isCancelled) break;
|
|
|
|
final groupId = rule.branchGroupId;
|
|
if (groupId != null && firedGroups.contains(groupId)) continue;
|
|
|
|
bool matches;
|
|
if (rule.isElseBranch) {
|
|
matches = true; // else fires unconditionally (group not yet consumed)
|
|
} else {
|
|
matches = _evaluateConditions(rule, email);
|
|
}
|
|
|
|
if (matches) {
|
|
_applyActions(rule.actions, ctx);
|
|
if (groupId != null) firedGroups.add(groupId);
|
|
if (ctx.isCancelled) break;
|
|
}
|
|
}
|
|
|
|
// Implicit keep: if no fileinto/discard was reached, email stays in inbox.
|
|
return ctx;
|
|
}
|
|
|
|
bool _evaluateConditions(SieveRule rule, SieveEmailContext email) {
|
|
if (rule.conditions.isEmpty) return true;
|
|
return switch (rule.joinType) {
|
|
'allof' => rule.conditions.every((c) => _evalCondition(c, email)),
|
|
'anyof' => rule.conditions.any((c) => _evalCondition(c, email)),
|
|
_ => rule.conditions.length == 1 &&
|
|
_evalCondition(rule.conditions.first, email),
|
|
};
|
|
}
|
|
|
|
bool _evalCondition(SieveCondition cond, SieveEmailContext email) {
|
|
return switch (cond) {
|
|
final HeaderCondition c => _evalHeader(c, email),
|
|
final SizeCondition c => _evalSize(c, email),
|
|
};
|
|
}
|
|
|
|
bool _evalHeader(HeaderCondition cond, SieveEmailContext email) {
|
|
for (final header in cond.headers) {
|
|
final values = email.getHeader(header);
|
|
for (final value in values) {
|
|
for (final key in cond.keyList) {
|
|
if (_matchString(value, cond.matchType, key)) return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool _evalSize(SizeCondition cond, SieveEmailContext email) {
|
|
return switch (cond.comparison) {
|
|
':over' => email.sizeBytes > cond.bytes,
|
|
':under' => email.sizeBytes < cond.bytes,
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
bool _matchString(String value, String matchType, String key) {
|
|
final v = value.toLowerCase();
|
|
final k = key.toLowerCase();
|
|
return switch (matchType) {
|
|
':contains' => k.isEmpty || v.contains(k),
|
|
':is' => v == k,
|
|
':matches' => _globMatch(v, k),
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
bool _globMatch(String value, String pattern) {
|
|
final regexStr = RegExp.escape(
|
|
pattern,
|
|
).replaceAll(r'\*', '.*').replaceAll(r'\?', '.');
|
|
return RegExp('^$regexStr\$').hasMatch(value);
|
|
}
|
|
|
|
void _applyActions(List<SieveAction> actions, SieveExecutionContext ctx) {
|
|
for (final action in actions) {
|
|
switch (action) {
|
|
case final FileIntoAction a:
|
|
ctx.targetFolders.add(a.folder);
|
|
ctx.keepInInbox = false;
|
|
case DiscardAction():
|
|
ctx.isCancelled = true;
|
|
ctx.keepInInbox = false;
|
|
return;
|
|
case KeepAction():
|
|
ctx.keepInInbox = true;
|
|
case MarkAsSeenAction():
|
|
ctx.flagsToAdd.add(r'\Seen');
|
|
case final FlagAction a:
|
|
ctx.flagsToAdd.addAll(a.flags);
|
|
}
|
|
}
|
|
}
|
|
}
|