feat: allow manual entry of glob patterns for trusted image senders (#480)
This commit was merged in pull request #480.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:sharedinbox/core/sieve/sieve_actions.dart';
|
||||
import 'package:sharedinbox/core/sieve/sieve_conditions.dart';
|
||||
import 'package:sharedinbox/core/sieve/sieve_rule.dart';
|
||||
import 'package:sharedinbox/core/utils/glob_match.dart';
|
||||
|
||||
/// A lightweight email representation used by [SieveInterpreter].
|
||||
/// Header names are lower-cased.
|
||||
@@ -102,18 +103,11 @@ class SieveInterpreter {
|
||||
return switch (matchType) {
|
||||
':contains' => k.isEmpty || v.contains(k),
|
||||
':is' => v == k,
|
||||
':matches' => _globMatch(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) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/// Returns true if [value] matches the glob [pattern].
|
||||
///
|
||||
/// Supports `*` (any number of characters) and `?` (exactly one character).
|
||||
/// The comparison is case-insensitive, which is appropriate for email addresses.
|
||||
bool globMatch(String value, String pattern) {
|
||||
final regexStr =
|
||||
RegExp.escape(pattern).replaceAll(r'\*', '.*').replaceAll(r'\?', '.');
|
||||
return RegExp('^$regexStr\$', caseSensitive: false).hasMatch(value);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import 'package:sharedinbox/core/models/note.dart';
|
||||
import 'package:sharedinbox/core/models/undo_action.dart';
|
||||
import 'package:sharedinbox/core/models/user_preferences.dart';
|
||||
import 'package:sharedinbox/core/utils/format_utils.dart';
|
||||
import 'package:sharedinbox/core/utils/glob_match.dart';
|
||||
import 'package:sharedinbox/core/utils/html_utils.dart';
|
||||
import 'package:sharedinbox/di.dart';
|
||||
import 'package:sharedinbox/ui/screens/email_action_helpers.dart';
|
||||
@@ -208,8 +209,8 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
||||
final senderEmail = header?.from.isNotEmpty == true
|
||||
? header!.from.first.email.toLowerCase()
|
||||
: null;
|
||||
final isTrusted =
|
||||
senderEmail != null && trustedSenders.contains(senderEmail);
|
||||
final isTrusted = senderEmail != null &&
|
||||
trustedSenders.any((p) => globMatch(senderEmail, p));
|
||||
final effectiveLoadImages = _loadRemoteImages || isTrusted;
|
||||
|
||||
return ListView(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:sharedinbox/core/models/email.dart';
|
||||
import 'package:sharedinbox/core/models/undo_action.dart';
|
||||
import 'package:sharedinbox/core/models/user_preferences.dart';
|
||||
import 'package:sharedinbox/core/utils/glob_match.dart';
|
||||
import 'package:sharedinbox/core/utils/html_utils.dart';
|
||||
import 'package:sharedinbox/di.dart';
|
||||
import 'package:sharedinbox/ui/widgets/secure_email_webview.dart';
|
||||
@@ -118,8 +119,8 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
|
||||
final senderEmail = widget.email.from.isNotEmpty
|
||||
? widget.email.from.first.email.toLowerCase()
|
||||
: null;
|
||||
final isTrusted =
|
||||
senderEmail != null && trustedSenders.contains(senderEmail);
|
||||
final isTrusted = senderEmail != null &&
|
||||
trustedSenders.any((p) => globMatch(senderEmail, p));
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
|
||||
@@ -16,6 +16,11 @@ class TrustedImageSendersScreen extends ConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Allowed addresses for images')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
tooltip: 'Add address',
|
||||
onPressed: () => _showAddDialog(context, ref),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: trustedSendersAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) =>
|
||||
@@ -26,7 +31,8 @@ class TrustedImageSendersScreen extends ConsumerWidget {
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'No addresses added yet. '
|
||||
'Tap "Load remote images" in an email to add the sender.',
|
||||
'Tap + to add an address or pattern (e.g. *@example.com), '
|
||||
'or tap "Load remote images" in an email to add the sender automatically.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -60,4 +66,61 @@ class TrustedImageSendersScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showAddDialog(BuildContext context, WidgetRef ref) async {
|
||||
final controller = TextEditingController();
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, setState) {
|
||||
return AlertDialog(
|
||||
title: const Text('Add allowed address'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email address or pattern',
|
||||
hintText: '*@example.com',
|
||||
helperText: '* matches any characters, e.g. *@example.com',
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
_addSender(ref, value);
|
||||
Navigator.of(ctx).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.text.trim().isEmpty
|
||||
? null
|
||||
: () {
|
||||
_addSender(ref, controller.text);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _addSender(WidgetRef ref, String value) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(userPreferencesRepositoryProvider)
|
||||
.addTrustedImageSender(value.trim()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user