Skip to content

Commit

Permalink
TF-3189 URL encoding should work
Browse files Browse the repository at this point in the history
  • Loading branch information
florentos17 committed Nov 15, 2024
1 parent e7a480b commit 89f6db4
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 9 deletions.
83 changes: 83 additions & 0 deletions core/lib/utils/mail/mail_address.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:core/domain/exceptions/address_exception.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/mail/domain.dart';
Expand All @@ -23,8 +24,15 @@ class MailAddress with EquatableMixin {
final String localPart;
final Domain domain;

static const String subaddressingLocalPartDelimiter = '+';

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) {
log('MailAddress::validate: Address = $address');
String localPart;
Expand Down Expand Up @@ -137,6 +145,54 @@ class MailAddress with EquatableMixin {
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);
}

String strictUrlEncode(String input) {
final StringBuffer encoded = StringBuffer();

for (int codeUnit in utf8.encode(input)) {
if ((codeUnit >= 0x30 && codeUnit <= 0x39) || // 0-9
(codeUnit >= 0x41 && codeUnit <= 0x5A) || // A-Z
(codeUnit >= 0x61 && codeUnit <= 0x7A)) { // a-z
encoded.writeCharCode(codeUnit);
} else {
encoded.write('%${codeUnit.toRadixString(16).padLeft(2, '0').toUpperCase()}');
}
}
return encoded.toString();
}

String asStringWithEncodedLocalPartDetails() {
String? localPartDetails = getLocalPartDetails();
if(localPartDetails == null) {
return asString();
} else {
return MailAddress.fromParts(
localPartWithoutDetails: getLocalPartWithoutDetails(),
localPartDetails: strictUrlEncode(localPartDetails),
domain: domain
).asString();
}
}

@override
String toString() {
return '$localPart@${domain.asString()}';
Expand Down Expand Up @@ -323,6 +379,9 @@ class MailAddress with EquatableMixin {
lpSB.write('.');
pos++;
lastCharDot = true;
} else if (postChar == subaddressingLocalPartDelimiter) {
// Start of local part details, jump to the `@`
pos = _parseLocalPartDetails(lpSB, address, pos);
} else if (postChar == '@') {
// End of local-part
break;
Expand Down Expand Up @@ -416,6 +475,30 @@ class MailAddress with EquatableMixin {
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();
// should validate localPartDetails

localPartSB.write(localPartDetails);
return pos;
}

@override
List<Object?> get props => [localPart, domain];
}
50 changes: 50 additions & 0 deletions core/test/utils/mail_address_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ void main() {
"संपर्क@डाटामेल.भारत"
];

final List<String> goodSubAddresses = [
"[email protected]",
"user+my [email protected]",
"user+Dossier d'été@domain.com"
];

group('MailAddress simple test', () {
test('MailAddress.validateAddress() should be return MailAddress when address valid', () {
MailAddress mailAddress = MailAddress.validateAddress('[email protected]');
Expand Down Expand Up @@ -165,5 +171,49 @@ void main() {
final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS);
expect(mailAddress.toString(), equals(GOOD_ADDRESS));
});

group('MailAddress.validateAddress() should not throw any exceptions with the list of good subaddress', () {
for (var arg in goodSubAddresses) {
test(arg, () {
expect(() => MailAddress.validateAddress(arg), returnsNormally);
});
}
});

test('MailAddress.encodeLocalPartDetails() should work with characters to encode', () {
final mailAddress = MailAddress.validateAddress("user+my [email protected]");
expect(mailAddress.asStringWithEncodedLocalPartDetails(), equals("user+my%[email protected]"));
});

test('MailAddress.encodeLocalPartDetails() should work with many characters to encode', () {
final mailAddress = MailAddress.validateAddress("user+Dossier d'été@domain.com");
expect(mailAddress.asStringWithEncodedLocalPartDetails(), equals("user+Dossier%20d%27%C3%A9t%C3%[email protected]"));
});

test('getLocalPartDetails() should work', () {
final mailAddress = MailAddress.validateAddress("[email protected]");
expect(mailAddress.getLocalPartDetails(), equals("details"));
});

test('getLocalPartWithoutDetails() should work', () {
final mailAddress = MailAddress.validateAddress("[email protected]");
expect(mailAddress.getLocalPartWithoutDetails(), equals("user"));
});

test('stripDetails() should work', () {
final mailAddress = MailAddress.validateAddress("[email protected]");
expect(mailAddress.stripDetails().asString(), equals("[email protected]"));
});

test('stripDetails() should work with encoded local part', () {
final mailAddress = MailAddress.validateAddress("user+Dossier%20d%27%C3%A9t%C3%[email protected]");
expect(mailAddress.stripDetails().asString(), equals("[email protected]"));
});

test('stripDetails() should work when local part needs encoding', () {
final mailAddress = MailAddress.validateAddress("user+super [email protected]");
expect(mailAddress.stripDetails().asString(), equals("[email protected]"));
});

});
}
33 changes: 25 additions & 8 deletions lib/features/composer/presentation/composer_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1614,9 +1614,20 @@ class ComposerController extends BaseController
.contains(inputEmail);
}

String? getDisplayName(MailAddress mailAddress) {
String? localPartDetails = mailAddress.getLocalPartDetails();
if(localPartDetails == null) {
return null;
} else {
return '${mailAddress.getLocalPartWithoutDetails()} [${mailAddress.getLocalPartDetails()}]';
}
}

void _autoCreateToEmailTag(String inputEmail) {
if (!_isDuplicatedRecipient(inputEmail, listToEmailAddress)) {
final emailAddress = EmailAddress(null, inputEmail);
MailAddress inputAddress = MailAddress.validateAddress(inputEmail);
String encodedInputEmail = inputAddress.asStringWithEncodedLocalPartDetails();
if (!_isDuplicatedRecipient(encodedInputEmail, listToEmailAddress)) {
final emailAddress = EmailAddress(getDisplayName(inputAddress), encodedInputEmail);
listToEmailAddress.add(emailAddress);
isInitialRecipient.value = true;
isInitialRecipient.refresh();
Expand All @@ -1630,8 +1641,10 @@ class ComposerController extends BaseController
}

void _autoCreateCcEmailTag(String inputEmail) {
if (!_isDuplicatedRecipient(inputEmail, listCcEmailAddress)) {
final emailAddress = EmailAddress(null, inputEmail);
MailAddress inputAddress = MailAddress.validateAddress(inputEmail);
String encodedInputEmail = inputAddress.asStringWithEncodedLocalPartDetails();
if (!_isDuplicatedRecipient(encodedInputEmail, listCcEmailAddress)) {
final emailAddress = EmailAddress(getDisplayName(inputAddress), encodedInputEmail);
listCcEmailAddress.add(emailAddress);
isInitialRecipient.value = true;
isInitialRecipient.refresh();
Expand All @@ -1644,8 +1657,10 @@ class ComposerController extends BaseController
}

void _autoCreateBccEmailTag(String inputEmail) {
if (!_isDuplicatedRecipient(inputEmail, listBccEmailAddress)) {
final emailAddress = EmailAddress(null, inputEmail);
MailAddress inputAddress = MailAddress.validateAddress(inputEmail);
String encodedInputEmail = inputAddress.asStringWithEncodedLocalPartDetails();
if (!_isDuplicatedRecipient(encodedInputEmail, listBccEmailAddress)) {
final emailAddress = EmailAddress(getDisplayName(inputAddress), encodedInputEmail);
listBccEmailAddress.add(emailAddress);
isInitialRecipient.value = true;
isInitialRecipient.refresh();
Expand All @@ -1658,8 +1673,10 @@ class ComposerController extends BaseController
}

void _autoCreateReplyToEmailTag(String inputEmail) {
if (!_isDuplicatedRecipient(inputEmail, listReplyToEmailAddress)) {
final emailAddress = EmailAddress(null, inputEmail);
MailAddress inputAddress = MailAddress.validateAddress(inputEmail);
String encodedInputEmail = inputAddress.asStringWithEncodedLocalPartDetails();
if (!_isDuplicatedRecipient(encodedInputEmail, listReplyToEmailAddress)) {
final emailAddress = EmailAddress(getDisplayName(inputAddress), encodedInputEmail);
listReplyToEmailAddress.add(emailAddress);
isInitialRecipient.value = true;
isInitialRecipient.refresh();
Expand Down
3 changes: 2 additions & 1 deletion lib/features/email/presentation/utils/email_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ class EmailUtils {

static bool isEmailAddressValid(String address) {
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) {
logError('EmailUtils::isEmailAddressValid: Exception = $e');
return false;
Expand Down

0 comments on commit 89f6db4

Please sign in to comment.