101 lines
3.3 KiB
Dart
101 lines
3.3 KiB
Dart
import 'package:sharedinbox/core/filter/filter_expression.dart';
|
|
import 'package:sharedinbox/core/sieve/sieve_actions.dart';
|
|
|
|
/// Serialises a [FilterGroup] + list of [SieveAction]s to a Sieve script
|
|
/// (RFC 5228 subset).
|
|
class SieveSerializer {
|
|
String serialize(FilterGroup filter, List<SieveAction> actions) {
|
|
final buf = StringBuffer();
|
|
final requires = _collectRequires(actions);
|
|
if (requires.isNotEmpty) {
|
|
buf.writeln(
|
|
'require [${requires.map((r) => '"$r"').join(', ')}];',
|
|
);
|
|
}
|
|
if (filter.isEmpty) {
|
|
for (final a in actions) {
|
|
buf.writeln(_serializeAction(a));
|
|
}
|
|
return buf.toString();
|
|
}
|
|
buf.write('if ');
|
|
buf.write(_serializeNode(filter));
|
|
buf.writeln(' {');
|
|
for (final a in actions) {
|
|
buf.writeln(' ${_serializeAction(a)}');
|
|
}
|
|
buf.writeln('}');
|
|
return buf.toString();
|
|
}
|
|
|
|
List<String> _collectRequires(List<SieveAction> actions) {
|
|
final req = <String>[];
|
|
for (final a in actions) {
|
|
if (a is FileIntoAction && !req.contains('fileinto')) req.add('fileinto');
|
|
if ((a is FlagAction || a is MarkAsSeenAction) &&
|
|
!req.contains('imap4flags')) {
|
|
req.add('imap4flags');
|
|
}
|
|
}
|
|
return req;
|
|
}
|
|
|
|
String _serializeNode(FilterNode node) => switch (node) {
|
|
final FilterLeaf leaf => _serializeLeaf(leaf),
|
|
final FilterGroup group => _serializeGroup(group),
|
|
};
|
|
|
|
String _serializeGroup(FilterGroup group) {
|
|
if (group.isEmpty) return 'true';
|
|
if (group.children.length == 1) return _serializeNode(group.children.first);
|
|
final op = group.operator == FilterOperator.and_ ? 'allof' : 'anyof';
|
|
final parts = group.children.map(_serializeNode).join(',\n ');
|
|
return '$op(\n $parts\n)';
|
|
}
|
|
|
|
String _serializeLeaf(FilterLeaf leaf) => switch (leaf.field) {
|
|
FilterField.from_ ||
|
|
FilterField.to ||
|
|
FilterField.cc =>
|
|
_serializeAddressLeaf(leaf),
|
|
FilterField.subject => _serializeHeaderLeaf(leaf),
|
|
FilterField.size => _serializeSizeLeaf(leaf),
|
|
};
|
|
|
|
String _serializeAddressLeaf(FilterLeaf leaf) {
|
|
final header = switch (leaf.field) {
|
|
FilterField.from_ => 'from',
|
|
FilterField.to => 'to',
|
|
FilterField.cc => 'cc',
|
|
_ => throw StateError('not an address field'),
|
|
};
|
|
return 'address ${_matchType(leaf.comparison)} "$header" "${_esc(leaf.value)}"';
|
|
}
|
|
|
|
String _serializeHeaderLeaf(FilterLeaf leaf) =>
|
|
'header ${_matchType(leaf.comparison)} "subject" "${_esc(leaf.value)}"';
|
|
|
|
String _serializeSizeLeaf(FilterLeaf leaf) {
|
|
final comp = leaf.comparison == FilterComparison.over ? ':over' : ':under';
|
|
return 'size $comp ${leaf.value}';
|
|
}
|
|
|
|
String _matchType(FilterComparison comp) => switch (comp) {
|
|
FilterComparison.contains => ':contains',
|
|
FilterComparison.is_ => ':is',
|
|
FilterComparison.matches => ':matches',
|
|
_ => ':contains',
|
|
};
|
|
|
|
String _serializeAction(SieveAction action) => switch (action) {
|
|
final FileIntoAction a => 'fileinto "${_esc(a.folder)}";',
|
|
KeepAction() => 'keep;',
|
|
DiscardAction() => 'discard;',
|
|
MarkAsSeenAction() => r'setflag "\\Seen";',
|
|
final FlagAction a =>
|
|
'addflag [${a.flags.map((f) => '"${_esc(f)}"').join(', ')}];',
|
|
};
|
|
|
|
String _esc(String s) => s.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
|
|
}
|