TF-4265 Additional config for include and exclude keywords

This commit is contained in:
dab246
2026-01-30 14:38:33 +07:00
committed by Dat H. Pham
parent 42fb24e6e5
commit 55e27c78ed
8 changed files with 187 additions and 2 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"includeList": [],
"excludeList": []
}
@@ -0,0 +1,77 @@
## Configuration for Attachment Keyword Detection
### Context
* When a user composes an email, the system detects if the content implies an attachment (e.g., "Please find attached...") but no file is attached.
* We need a flexible way to add specific keywords (Include) or ignore specific tokens (Exclude) without modifying the source code.
### How to config
1. Configuration File Location
* The configuration is managed in the JSON file: `configurations\attachment_keywords.json`
* This file must be declared in `pubspec.yaml` under `assets`.
2. JSON Structure
The file consists of two main arrays:
```json
{
"includeList": [
"invoice",
"estimate",
"báo giá"
],
"excludeList": [
"signature-logo",
"no-reply",
"icon-app"
]
}
```
* `includeList`: List of keywords to **add** to the detection dictionary.
* Used when the default dictionary (hardcoded in app) is missing specific business terms.
* Matches are case-insensitive.
* Example: Adding `"cv"` will trigger the alert for "Attached is my CV".
* `excludeList`: List of tokens to **ignore/block** from detection.
* Used to prevent false positives where a keyword exists but is part of a system string or signature.
* The filter checks the full surrounding token (e.g., if you exclude `"signature-logo"`, the text `"check signature-logo"` will be ignored even if it contains "signature").
3. Application Logic
* **Loading:** The file is loaded lazily and cached in memory upon the first request (Singleton pattern).
* **Execution Flow:**
1. User clicks "Send".
2. App loads `includeList` and merges it with the default multi-language dictionary.
3. Regex scans the text.
4. Matches are filtered against the `excludeList`.
5. If valid keywords remain and no file is attached, a warning dialog is shown.
4. Example Customization
If you want to support a new document type called "Blueprints" and ignore a specific CSS class in the email body:
**Modify `configurations\attachment_keywords.json`:**
```json
{
"includeList": [
"blueprints",
"bản vẽ"
],
"excludeList": [
"css-class-attachment",
"div-id-file"
]
}
```
@@ -890,6 +890,12 @@ class ComposerController extends BaseController
emailSubject: subjectEmail.value ?? '',
emailContent: emailContent,
);
if (!context.mounted) {
logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED');
return;
}
if (attachmentKeywords.isNotEmpty &&
uploadController.attachmentsUploaded.isEmpty) {
showAttachmentReminderModal(
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_text_detector.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/keyword_config_manager.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -16,8 +17,12 @@ extension AttachmentDetectionExtension on ComposerController {
try {
final fullContent = '$emailSubject $emailContent';
final plainText = HtmlUtils.extractPlainText(fullContent);
final keywords =
await AttachmentTextDetector.matchedKeywordsUnique(plainText);
final keywordConfig = await KeywordConfigManager().getConfig();
final keywords = await AttachmentTextDetector.matchedKeywordsUnique(
plainText,
includeList: keywordConfig.includeList,
excludeList: keywordConfig.excludeList,
);
if (keywords.isEmpty) {
return [];
} else {
@@ -0,0 +1,26 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/config/app_config_parser.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/keyword_config.dart';
class AttachmentKeywordsConfigurationParser
extends AppConfigParser<KeywordConfig> {
@override
Future<KeywordConfig> parse(String value) async {
try {
final jsonObject = jsonDecode(value);
return KeywordConfig.fromJson(jsonObject);
} catch (e) {
logWarning('AttachmentKeywordsConfigurationParser::parse(): $e');
rethrow;
}
}
@override
Future<KeywordConfig> parseData(ByteData data) {
throw UnimplementedError();
}
}
@@ -0,0 +1,43 @@
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/config/app_config_loader.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/keyword_config.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
class KeywordConfigManager {
static final KeywordConfigManager _instance = KeywordConfigManager._();
factory KeywordConfigManager() => _instance;
KeywordConfigManager._();
KeywordConfig? _cachedConfig;
AppConfigLoader? _appConfigLoader;
static const String _configPath =
AppConfig.attachmentKeywordsConfigurationPath;
Future<KeywordConfig> getConfig() async {
if (_cachedConfig != null) {
return _cachedConfig!;
}
try {
_appConfigLoader ??= AppConfigLoader();
_cachedConfig = await _appConfigLoader?.load<KeywordConfig>(
_configPath,
AttachmentKeywordsConfigurationParser(),
);
} catch (e) {
logWarning(
"KeywordConfigManager::getConfig:Error loading keyword config: $e");
_cachedConfig = const KeywordConfig();
}
return _cachedConfig!;
}
void clearCache() {
_cachedConfig = null;
}
}
@@ -0,0 +1,23 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'keyword_config.g.dart';
@JsonSerializable(includeIfNull: false, explicitToJson: true)
class KeywordConfig with EquatableMixin {
final List<String> includeList;
final List<String> excludeList;
const KeywordConfig({
this.includeList = const [],
this.excludeList = const [],
});
factory KeywordConfig.fromJson(Map<String, dynamic> json) =>
_$KeywordConfigFromJson(json);
Map<String, dynamic> toJson() => _$KeywordConfigToJson(this);
@override
List<Object?> get props => [includeList, excludeList];
}
+1
View File
@@ -12,6 +12,7 @@ class AppConfig {
static const int defaultLimitAutocomplete = 8;
static const String appDashboardConfigurationPath = "configurations/app_dashboard.json";
static const String attachmentKeywordsConfigurationPath = "configurations/attachment_keywords.json";
static const String iOSKeychainSharingGroupId = 'KUT463DS29.com.linagora.ios.teammail.shared';
static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions';
static const String saasPlatform = 'saas';