TF-3189 composer now correctly encodes subaddresses
This commit is contained in:
committed by
Dat H. Pham
parent
74fc8d5d72
commit
2a56764214
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:core/domain/exceptions/address_exception.dart';
|
import 'package:core/domain/exceptions/address_exception.dart';
|
||||||
import 'package:core/utils/app_logger.dart';
|
import 'package:core/utils/app_logger.dart';
|
||||||
import 'package:core/utils/mail/domain.dart';
|
import 'package:core/utils/mail/domain.dart';
|
||||||
@@ -23,8 +24,15 @@ class MailAddress with EquatableMixin {
|
|||||||
final String localPart;
|
final String localPart;
|
||||||
final Domain domain;
|
final Domain domain;
|
||||||
|
|
||||||
|
static const String subaddressingLocalPartDelimiter = '+';
|
||||||
|
|
||||||
MailAddress({required this.localPart, required this.domain});
|
MailAddress({required this.localPart, required this.domain});
|
||||||
|
|
||||||
|
MailAddress.fromParts({required String localPartWithoutDetails, required String localPartDetails, required this.domain}) : localPart =
|
||||||
|
localPartDetails.isEmpty
|
||||||
|
? localPartWithoutDetails
|
||||||
|
: '$localPartWithoutDetails$subaddressingLocalPartDelimiter$localPartDetails';
|
||||||
|
|
||||||
factory MailAddress.validateAddress(String address) {
|
factory MailAddress.validateAddress(String address) {
|
||||||
log('MailAddress::validate: Address = $address');
|
log('MailAddress::validate: Address = $address');
|
||||||
String localPart;
|
String localPart;
|
||||||
@@ -137,6 +145,89 @@ class MailAddress with EquatableMixin {
|
|||||||
return localPart;
|
return localPart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? getLocalPartDetails() {
|
||||||
|
int separatorPosition = localPart.indexOf(subaddressingLocalPartDelimiter);
|
||||||
|
if (separatorPosition <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return localPart.substring(separatorPosition + subaddressingLocalPartDelimiter.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLocalPartWithoutDetails() {
|
||||||
|
int separatorPosition = localPart.indexOf(subaddressingLocalPartDelimiter);
|
||||||
|
if (separatorPosition <= 0) {
|
||||||
|
return localPart;
|
||||||
|
}
|
||||||
|
return localPart.substring(0, separatorPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
MailAddress stripDetails() {
|
||||||
|
return MailAddress(localPart: getLocalPartWithoutDetails(), domain: domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot use Uri.encodeComponent because it is meant to be compliant with RFC2396
|
||||||
|
// eg `-_.!~*'()` are not encoded, but we want `!*'()` to be
|
||||||
|
static final _needsNoEncoding = RegExp(r'^[a-zA-Z0-9._~-]+$');
|
||||||
|
|
||||||
|
// this table is adapted from `_unreserved2396Table` found at
|
||||||
|
// https://github.com/dart-lang/sdk/blob/58f9beb6d4ec9e93430454bb96c0b8f068d0b0bc/sdk/lib/core/uri.dart#L3382
|
||||||
|
static const _customUnreservedTable = <int>[
|
||||||
|
// LSB MSB
|
||||||
|
// | |
|
||||||
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
||||||
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
||||||
|
// -.
|
||||||
|
0x6000, // 0x20 - 0x2f 0000000000000110
|
||||||
|
// 0123456789
|
||||||
|
0x03ff, // 0x30 - 0x3f 1111111111000000
|
||||||
|
// ABCDEFGHIJKLMNO
|
||||||
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
||||||
|
// PQRSTUVWXYZ _
|
||||||
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
||||||
|
// abcdefghijklmno
|
||||||
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
||||||
|
// pqrstuvwxyz ~
|
||||||
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
||||||
|
];
|
||||||
|
|
||||||
|
// this method is adapted from `_uriEncode()` found at:
|
||||||
|
// https://github.com/dart-lang/sdk/blob/bb8db16297e6b9994b08ecae6ee1dd45a0be587e/sdk/lib/_internal/wasm/lib/uri_patch.dart#L49
|
||||||
|
static String customUriEncode(String text) {
|
||||||
|
if (_needsNoEncoding.hasMatch(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the string into bytes then generate an ASCII only string
|
||||||
|
// by percent encoding selected bytes.
|
||||||
|
StringBuffer result = StringBuffer('');
|
||||||
|
var bytes = utf8.encode(text);
|
||||||
|
for (int byte in bytes) {
|
||||||
|
if (byte < 128 &&
|
||||||
|
((_customUnreservedTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) {
|
||||||
|
result.writeCharCode(byte);
|
||||||
|
} else {
|
||||||
|
const String hexDigits = '0123456789ABCDEF';
|
||||||
|
result.write('%');
|
||||||
|
result.write(hexDigits[(byte >> 4) & 0x0f]);
|
||||||
|
result.write(hexDigits[byte & 0x0f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String asEncodedString() {
|
||||||
|
String? localPartDetails = getLocalPartDetails();
|
||||||
|
if(localPartDetails == null) {
|
||||||
|
return asString();
|
||||||
|
} else {
|
||||||
|
return MailAddress.fromParts(
|
||||||
|
localPartWithoutDetails: getLocalPartWithoutDetails(),
|
||||||
|
localPartDetails: customUriEncode(localPartDetails),
|
||||||
|
domain: domain
|
||||||
|
).asString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '$localPart@${domain.asString()}';
|
return '$localPart@${domain.asString()}';
|
||||||
@@ -323,6 +414,10 @@ class MailAddress with EquatableMixin {
|
|||||||
lpSB.write('.');
|
lpSB.write('.');
|
||||||
pos++;
|
pos++;
|
||||||
lastCharDot = true;
|
lastCharDot = true;
|
||||||
|
} else if (postChar == subaddressingLocalPartDelimiter) {
|
||||||
|
// Start of local part details, jump to the `@`
|
||||||
|
lpSB.write(subaddressingLocalPartDelimiter);
|
||||||
|
pos = _parseLocalPartDetails(lpSB, address, pos+1);
|
||||||
} else if (postChar == '@') {
|
} else if (postChar == '@') {
|
||||||
// End of local-part
|
// End of local-part
|
||||||
break;
|
break;
|
||||||
@@ -416,6 +511,39 @@ class MailAddress with EquatableMixin {
|
|||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int _parseLocalPartDetails(StringBuffer localPartSB, String address, int pos) {
|
||||||
|
StringBuffer localPartDetailsSB = StringBuffer();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var postChar = address[pos];
|
||||||
|
if (postChar == '@') {
|
||||||
|
// End of local-part-details
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
localPartDetailsSB.write(postChar);
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String localPartDetails = localPartDetailsSB.toString();
|
||||||
|
if (localPartDetails.isEmpty || localPartDetails.trim().isEmpty) {
|
||||||
|
throw AddressException("target mailbox name should not be empty");
|
||||||
|
}
|
||||||
|
if (localPartDetails.startsWith('#')) {
|
||||||
|
throw AddressException("target mailbox name should not start with #");
|
||||||
|
}
|
||||||
|
final forbiddenChars = RegExp(r'[*\r\n]');
|
||||||
|
if (forbiddenChars.hasMatch(localPartDetails)) {
|
||||||
|
throw AddressException("target mailbox name should not contain special characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
localPartSB.write(localPartDetails);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [localPart, domain];
|
List<Object?> get props => [localPart, domain];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ void main() {
|
|||||||
"Abc.123@example.com",
|
"Abc.123@example.com",
|
||||||
"user+mailbox/department=shipping@example.com",
|
"user+mailbox/department=shipping@example.com",
|
||||||
"user+mailbox@example.com",
|
"user+mailbox@example.com",
|
||||||
|
"user+folder@james.apache.org",
|
||||||
|
"user+my folder@domain.com",
|
||||||
|
"user+Dossier d'été@domain.com",
|
||||||
"\"Abc@def\"@example.com",
|
"\"Abc@def\"@example.com",
|
||||||
"\"Fred Bloggs\"@example.com",
|
"\"Fred Bloggs\"@example.com",
|
||||||
"\"Joe.\\Blow\"@example.com",
|
"\"Joe.\\Blow\"@example.com",
|
||||||
@@ -56,6 +59,10 @@ void main() {
|
|||||||
"server-dev@#123.apache.org",
|
"server-dev@#123.apache.org",
|
||||||
"server-dev@[127.0.1.1.1]",
|
"server-dev@[127.0.1.1.1]",
|
||||||
"server-dev@[127.0.1.-1]",
|
"server-dev@[127.0.1.-1]",
|
||||||
|
"user+@domain.com",
|
||||||
|
"user+ @domain.com",
|
||||||
|
"user+#folder@domain.com",
|
||||||
|
"user+test-_.!~*'() @domain.com",
|
||||||
"\"a..b\"@domain.com", // jakarta.mail is unable to handle this so we better reject it
|
"\"a..b\"@domain.com", // jakarta.mail is unable to handle this so we better reject it
|
||||||
"server-dev\\.@james.apache.org", // jakarta.mail is unable to handle this so we better reject it
|
"server-dev\\.@james.apache.org", // jakarta.mail is unable to handle this so we better reject it
|
||||||
"a..b@domain.com",
|
"a..b@domain.com",
|
||||||
@@ -165,5 +172,46 @@ void main() {
|
|||||||
final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS);
|
final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS);
|
||||||
expect(mailAddress.toString(), equals(GOOD_ADDRESS));
|
expect(mailAddress.toString(), equals(GOOD_ADDRESS));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MailAddress.encodeLocalPartDetails() should work with characters to encode', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+my folder@domain.com");
|
||||||
|
expect(mailAddress.asEncodedString(), equals("user+my%20folder@domain.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MailAddress.encodeLocalPartDetails() should work with many characters to encode', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+Dossier d'été@domain.com");
|
||||||
|
expect(mailAddress.asEncodedString(), equals("user+Dossier%20d%27%C3%A9t%C3%A9@domain.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MailAddress.encodeLocalPartDetails() should encode the rights characters', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+test-_.!~'() @domain.com");
|
||||||
|
expect(mailAddress.asEncodedString(), equals("user+test-_.%21~%27%28%29%20@domain.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getLocalPartDetails() should work', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+details@domain.com");
|
||||||
|
expect(mailAddress.getLocalPartDetails(), equals("details"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getLocalPartWithoutDetails() should work', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+details@domain.com");
|
||||||
|
expect(mailAddress.getLocalPartWithoutDetails(), equals("user"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stripDetails() should work', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+details@domain.com");
|
||||||
|
expect(mailAddress.stripDetails().asString(), equals("user@domain.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stripDetails() should work with encoded local part', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+Dossier%20d%27%C3%A9t%C3%A9@domain.com");
|
||||||
|
expect(mailAddress.stripDetails().asString(), equals("user@domain.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stripDetails() should work when local part needs encoding', () {
|
||||||
|
final mailAddress = MailAddress.validateAddress("user+super folder@domain.com");
|
||||||
|
expect(mailAddress.stripDetails().asString(), equals("user@domain.com"));
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/get_outb
|
|||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/mail_address_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
|
||||||
@@ -1634,33 +1635,18 @@ class ComposerController extends BaseController
|
|||||||
final inputCcEmail = ccEmailAddressController.text;
|
final inputCcEmail = ccEmailAddressController.text;
|
||||||
final inputBccEmail = bccEmailAddressController.text;
|
final inputBccEmail = bccEmailAddressController.text;
|
||||||
final inputReplyToEmail = replyToEmailAddressController.text;
|
final inputReplyToEmail = replyToEmailAddressController.text;
|
||||||
log('ComposerController::_autoCreateEmailTag:inputToEmail = $inputToEmail | inputCcEmail = $inputCcEmail | inputBccEmail = $inputBccEmail');
|
|
||||||
if (inputToEmail.trim().isNotEmpty) {
|
if (inputToEmail.isNotEmpty) {
|
||||||
_autoCreateEmailTagForRecipientField(
|
_autoCreateToEmailTag(MailAddress.validateAddress(inputToEmail));
|
||||||
prefixEmail: PrefixEmailAddress.to,
|
|
||||||
inputText: inputToEmail,
|
|
||||||
listEmailAddress: listToEmailAddress,
|
|
||||||
keyEmailTagEditor: keyToEmailTagEditor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (inputCcEmail.trim().isNotEmpty) {
|
if (inputCcEmail.isNotEmpty) {
|
||||||
_autoCreateEmailTagForRecipientField(
|
_autoCreateCcEmailTag(MailAddress.validateAddress(inputCcEmail));
|
||||||
prefixEmail: PrefixEmailAddress.cc,
|
|
||||||
inputText: inputCcEmail,
|
|
||||||
listEmailAddress: listCcEmailAddress,
|
|
||||||
keyEmailTagEditor: keyCcEmailTagEditor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (inputBccEmail.trim().isNotEmpty) {
|
if (inputBccEmail.isNotEmpty) {
|
||||||
_autoCreateEmailTagForRecipientField(
|
_autoCreateBccEmailTag(MailAddress.validateAddress(inputBccEmail));
|
||||||
prefixEmail: PrefixEmailAddress.bcc,
|
|
||||||
inputText: inputBccEmail,
|
|
||||||
listEmailAddress: listBccEmailAddress,
|
|
||||||
keyEmailTagEditor: keyBccEmailTagEditor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (inputReplyToEmail.isNotEmpty) {
|
if (inputReplyToEmail.isNotEmpty) {
|
||||||
_autoCreateReplyToEmailTag(inputReplyToEmail);
|
_autoCreateReplyToEmailTag(MailAddress.validateAddress(inputReplyToEmail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,59 +1657,49 @@ class ComposerController extends BaseController
|
|||||||
.contains(inputEmail);
|
.contains(inputEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _autoCreateEmailTagForRecipientField({
|
void _autoCreateToEmailTag(MailAddress inputMailAddress) {
|
||||||
required PrefixEmailAddress prefixEmail,
|
if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listToEmailAddress)) {
|
||||||
required String inputText,
|
listToEmailAddress.add(inputMailAddress.asEmailAddress());
|
||||||
required List<EmailAddress> listEmailAddress,
|
isInitialRecipient.value = true;
|
||||||
required GlobalKey<TagsEditorState> keyEmailTagEditor,
|
isInitialRecipient.refresh();
|
||||||
}) {
|
_updateStatusEmailSendButton();
|
||||||
log('ComposerController::_autoCreateEmailTagForRecipientField:prefixEmail = $prefixEmail | inputText = $inputText | listEmailAddress = $listEmailAddress');
|
|
||||||
switch(prefixEmail) {
|
|
||||||
case PrefixEmailAddress.to:
|
|
||||||
case PrefixEmailAddress.cc:
|
|
||||||
case PrefixEmailAddress.bcc:
|
|
||||||
final listString = StringConvert.extractStrings(inputText).toSet();
|
|
||||||
if (listString.isEmpty && !_isDuplicatedRecipient(inputText, listEmailAddress)) {
|
|
||||||
final emailAddress = EmailAddress(null, inputText);
|
|
||||||
listEmailAddress.add(emailAddress);
|
|
||||||
isInitialRecipient.value = true;
|
|
||||||
isInitialRecipient.refresh();
|
|
||||||
_updateStatusEmailSendButton();
|
|
||||||
keyEmailTagEditor.currentState?.resetTextField();
|
|
||||||
Future.delayed(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
keyEmailTagEditor.currentState?.closeSuggestionBox,
|
|
||||||
);
|
|
||||||
} else if (listString.isNotEmpty) {
|
|
||||||
final listStringNotExist = listString
|
|
||||||
.where((text) => !_isDuplicatedRecipient(text, listEmailAddress))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (listStringNotExist.isNotEmpty) {
|
|
||||||
final listAddress = listStringNotExist
|
|
||||||
.map((value) => EmailAddress(null, value))
|
|
||||||
.toList();
|
|
||||||
listEmailAddress.addAll(listAddress);
|
|
||||||
isInitialRecipient.value = true;
|
|
||||||
isInitialRecipient.refresh();
|
|
||||||
_updateStatusEmailSendButton();
|
|
||||||
keyEmailTagEditor.currentState?.resetTextField();
|
|
||||||
Future.delayed(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
keyEmailTagEditor.currentState?.closeSuggestionBox,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
log('ComposerController::_autoCreateToEmailTag(): STATE: ${keyToEmailTagEditor.currentState}');
|
||||||
|
keyToEmailTagEditor.currentState?.resetTextField();
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
keyToEmailTagEditor.currentState?.closeSuggestionBox();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _autoCreateReplyToEmailTag(String inputEmail) {
|
void _autoCreateCcEmailTag(MailAddress inputMailAddress) {
|
||||||
if (!_isDuplicatedRecipient(inputEmail, listReplyToEmailAddress)) {
|
if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listCcEmailAddress)) {
|
||||||
final emailAddress = EmailAddress(null, inputEmail);
|
listCcEmailAddress.add(inputMailAddress.asEmailAddress());
|
||||||
listReplyToEmailAddress.add(emailAddress);
|
isInitialRecipient.value = true;
|
||||||
|
isInitialRecipient.refresh();
|
||||||
|
_updateStatusEmailSendButton();
|
||||||
|
}
|
||||||
|
keyCcEmailTagEditor.currentState?.resetTextField();
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
keyCcEmailTagEditor.currentState?.closeSuggestionBox();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _autoCreateBccEmailTag(MailAddress inputMailAddress) {
|
||||||
|
if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listBccEmailAddress)) {
|
||||||
|
listBccEmailAddress.add(inputMailAddress.asEmailAddress());
|
||||||
|
isInitialRecipient.value = true;
|
||||||
|
isInitialRecipient.refresh();
|
||||||
|
_updateStatusEmailSendButton();
|
||||||
|
}
|
||||||
|
keyBccEmailTagEditor.currentState?.resetTextField();
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
keyBccEmailTagEditor.currentState?.closeSuggestionBox();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _autoCreateReplyToEmailTag(MailAddress inputMailAddress) {
|
||||||
|
if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listReplyToEmailAddress)) {
|
||||||
|
listReplyToEmailAddress.add(inputMailAddress.asEmailAddress());
|
||||||
isInitialRecipient.value = true;
|
isInitialRecipient.value = true;
|
||||||
isInitialRecipient.refresh();
|
isInitialRecipient.refresh();
|
||||||
_updateStatusEmailSendButton();
|
_updateStatusEmailSendButton();
|
||||||
@@ -1801,44 +1777,29 @@ class ComposerController extends BaseController
|
|||||||
case PrefixEmailAddress.to:
|
case PrefixEmailAddress.to:
|
||||||
toAddressExpandMode.value = ExpandMode.COLLAPSE;
|
toAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputToEmail = toEmailAddressController.text;
|
final inputToEmail = toEmailAddressController.text;
|
||||||
if (inputToEmail.trim().isNotEmpty) {
|
if (inputToEmail.isNotEmpty) {
|
||||||
_autoCreateEmailTagForRecipientField(
|
_autoCreateToEmailTag(MailAddress.validateAddress(inputToEmail));
|
||||||
prefixEmail: PrefixEmailAddress.to,
|
|
||||||
inputText: inputToEmail,
|
|
||||||
listEmailAddress: listToEmailAddress,
|
|
||||||
keyEmailTagEditor: keyToEmailTagEditor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PrefixEmailAddress.cc:
|
case PrefixEmailAddress.cc:
|
||||||
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputCcEmail = ccEmailAddressController.text;
|
final inputCcEmail = ccEmailAddressController.text;
|
||||||
if (inputCcEmail.trim().isNotEmpty) {
|
if (inputCcEmail.isNotEmpty) {
|
||||||
_autoCreateEmailTagForRecipientField(
|
_autoCreateCcEmailTag(MailAddress.validateAddress(inputCcEmail));
|
||||||
prefixEmail: PrefixEmailAddress.cc,
|
|
||||||
inputText: inputCcEmail,
|
|
||||||
listEmailAddress: listCcEmailAddress,
|
|
||||||
keyEmailTagEditor: keyCcEmailTagEditor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PrefixEmailAddress.bcc:
|
case PrefixEmailAddress.bcc:
|
||||||
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputBccEmail = bccEmailAddressController.text;
|
final inputBccEmail = bccEmailAddressController.text;
|
||||||
if (inputBccEmail.trim().isNotEmpty) {
|
if (inputBccEmail.isNotEmpty) {
|
||||||
_autoCreateEmailTagForRecipientField(
|
_autoCreateBccEmailTag(MailAddress.validateAddress(inputBccEmail));
|
||||||
prefixEmail: PrefixEmailAddress.bcc,
|
|
||||||
inputText: inputBccEmail,
|
|
||||||
listEmailAddress: listBccEmailAddress,
|
|
||||||
keyEmailTagEditor: keyBccEmailTagEditor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PrefixEmailAddress.replyTo:
|
case PrefixEmailAddress.replyTo:
|
||||||
replyToAddressExpandMode.value = ExpandMode.COLLAPSE;
|
replyToAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputReplyToEmail = replyToEmailAddressController.text;
|
final inputReplyToEmail = replyToEmailAddressController.text;
|
||||||
if (inputReplyToEmail.isNotEmpty) {
|
if (inputReplyToEmail.isNotEmpty) {
|
||||||
_autoCreateReplyToEmailTag(inputReplyToEmail);
|
_autoCreateReplyToEmailTag(MailAddress.validateAddress(inputReplyToEmail));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
|
import 'package:core/core.dart';
|
||||||
|
|
||||||
|
extension MailAddressExtension on MailAddress {
|
||||||
|
String? get getDisplayName {
|
||||||
|
String? localPartDetails = getLocalPartDetails();
|
||||||
|
if(localPartDetails == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return '${getLocalPartWithoutDetails()} [${getLocalPartDetails()}]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailAddress asEmailAddress() {
|
||||||
|
return EmailAddress(getDisplayName, asEncodedString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:core/presentation/resources/image_paths.dart';
|
|||||||
import 'package:core/presentation/utils/responsive_utils.dart';
|
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
import 'package:core/utils/app_logger.dart';
|
import 'package:core/utils/app_logger.dart';
|
||||||
|
import 'package:core/utils/mail/mail_address.dart';
|
||||||
import 'package:core/utils/platform_info.dart';
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:core/utils/string_convert.dart';
|
import 'package:core/utils/string_convert.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'package:model/extensions/email_address_extension.dart';
|
|||||||
import 'package:model/mailbox/expand_mode.dart';
|
import 'package:model/mailbox/expand_mode.dart';
|
||||||
import 'package:super_tag_editor/tag_editor.dart';
|
import 'package:super_tag_editor/tag_editor.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/mail_address_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart';
|
||||||
@@ -228,7 +230,7 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
|
|||||||
return RecipientSuggestionItemWidget(
|
return RecipientSuggestionItemWidget(
|
||||||
imagePaths: widget.imagePaths,
|
imagePaths: widget.imagePaths,
|
||||||
suggestionState: suggestionEmailAddress.state,
|
suggestionState: suggestionEmailAddress.state,
|
||||||
emailAddress: suggestionEmailAddress.emailAddress,
|
emailAddress: MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress).asEmailAddress(),
|
||||||
suggestionValid: suggestionValid,
|
suggestionValid: suggestionValid,
|
||||||
highlight: highlight,
|
highlight: highlight,
|
||||||
onSelectedAction: (emailAddress) {
|
onSelectedAction: (emailAddress) {
|
||||||
@@ -319,7 +321,7 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
|
|||||||
return RecipientSuggestionItemWidget(
|
return RecipientSuggestionItemWidget(
|
||||||
imagePaths: widget.imagePaths,
|
imagePaths: widget.imagePaths,
|
||||||
suggestionState: suggestionEmailAddress.state,
|
suggestionState: suggestionEmailAddress.state,
|
||||||
emailAddress: suggestionEmailAddress.emailAddress,
|
emailAddress: MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress).asEmailAddress(),
|
||||||
suggestionValid: suggestionValid,
|
suggestionValid: suggestionValid,
|
||||||
highlight: highlight,
|
highlight: highlight,
|
||||||
onSelectedAction: (emailAddress) {
|
onSelectedAction: (emailAddress) {
|
||||||
@@ -523,8 +525,9 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
|
|||||||
SuggestionEmailAddress suggestionEmailAddress,
|
SuggestionEmailAddress suggestionEmailAddress,
|
||||||
StateSetter stateSetter
|
StateSetter stateSetter
|
||||||
) {
|
) {
|
||||||
if (!_isDuplicatedRecipient(suggestionEmailAddress.emailAddress.emailAddress)) {
|
MailAddress mailAddress = MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress);
|
||||||
stateSetter(() => _currentListEmailAddress.add(suggestionEmailAddress.emailAddress));
|
if (!_isDuplicatedRecipient(mailAddress.asEncodedString())) {
|
||||||
|
stateSetter(() => _currentListEmailAddress.add(mailAddress.asEmailAddress()));
|
||||||
_updateListEmailAddressAction();
|
_updateListEmailAddressAction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,9 +539,10 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
|
|||||||
|
|
||||||
void _createMailTag(String value, StateSetter stateSetter) {
|
void _createMailTag(String value, StateSetter stateSetter) {
|
||||||
final listString = StringConvert.extractStrings(value.trim()).toSet();
|
final listString = StringConvert.extractStrings(value.trim()).toSet();
|
||||||
|
MailAddress mailAddress = MailAddress.validateAddress(value.trim());
|
||||||
|
|
||||||
if (listString.isEmpty && !_isDuplicatedRecipient(value)) {
|
if (listString.isEmpty && !_isDuplicatedRecipient(mailAddress.asEncodedString())) {
|
||||||
stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, value)));
|
stateSetter(() => _currentListEmailAddress.add(mailAddress.asEmailAddress()));
|
||||||
_updateListEmailAddressAction();
|
_updateListEmailAddressAction();
|
||||||
} else if (listString.isNotEmpty) {
|
} else if (listString.isNotEmpty) {
|
||||||
final listStringNotExist = listString
|
final listStringNotExist = listString
|
||||||
@@ -560,9 +564,9 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
|
|||||||
String value,
|
String value,
|
||||||
StateSetter stateSetter
|
StateSetter stateSetter
|
||||||
) {
|
) {
|
||||||
final textTrim = value.trim();
|
MailAddress mailAddress = MailAddress.validateAddress(value.trim());
|
||||||
if (!_isDuplicatedRecipient(textTrim)) {
|
if (!_isDuplicatedRecipient(mailAddress.asEncodedString())) {
|
||||||
stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, textTrim)));
|
stateSetter(() => _currentListEmailAddress.add(mailAddress.asEmailAddress()));
|
||||||
_updateListEmailAddressAction();
|
_updateListEmailAddressAction();
|
||||||
}
|
}
|
||||||
_gapBetweenTagChangedAndFindSuggestion = Timer(
|
_gapBetweenTagChangedAndFindSuggestion = Timer(
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ class EmailUtils {
|
|||||||
|
|
||||||
static bool isEmailAddressValid(String address) {
|
static bool isEmailAddressValid(String address) {
|
||||||
try {
|
try {
|
||||||
return GetUtils.isEmail(address) && MailAddress.validateAddress(address).asString().isNotEmpty;
|
MailAddress mailAddress = MailAddress.validateAddress(address);
|
||||||
|
return GetUtils.isEmail(mailAddress.stripDetails().asString()) && mailAddress.asString().isNotEmpty;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logError('EmailUtils::isEmailAddressValid: Exception = $e');
|
logError('EmailUtils::isEmailAddressValid: Exception = $e');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user