Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSUE-3189 front end support for subaddressing #3241

Merged
merged 6 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/images/ic_copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/images/ic_subaddressing_allow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions assets/images/ic_subaddressing_disallow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions core/lib/presentation/resources/image_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ class ImagePaths {
String get icBadSignature => _getImagePath('ic_bad_signature.svg');
String get icDeleteSelection => _getImagePath('ic_delete_selection.svg');
String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg');
String get icCopy => _getImagePath('ic_copy.svg');
String get icSubaddressingAllow => _getImagePath('ic_subaddressing_allow.svg');
String get icSubaddressingDisallow => _getImagePath('ic_subaddressing_disallow.svg');

String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ class ConfirmDialogBuilder {

Key? _key;
String _title = '';
String _content = '';
String _textContent = '';
String _confirmText = '';
String _cancelText = '';
Widget? _iconWidget;
Widget? _additionalWidgetContent;
Color? _colorCancelButton;
Color? _colorConfirmButton;
TextStyle? _styleTextCancelButton;
Expand Down Expand Up @@ -63,7 +64,11 @@ class ConfirmDialogBuilder {
}

void content(String content) {
_content = content;
_textContent = content;
}

void addWidgetContent(Widget? icon) {
_additionalWidgetContent = icon;
}

void addIcon(Widget? icon) {
Expand Down Expand Up @@ -210,11 +215,11 @@ class ConfirmDialogBuilder {
)
)
),
if (_content.isNotEmpty)
if (_textContent.isNotEmpty)
Padding(
padding: _paddingContent ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Center(
child: Text(_content,
child: Text(_textContent,
textAlign: TextAlign.center,
style: _styleContent ?? const TextStyle(fontSize: 17.0, color: AppColor.colorMessageDialog)
),
Expand All @@ -233,6 +238,11 @@ class ConfirmDialogBuilder {
),
),
),
if (_additionalWidgetContent != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _additionalWidgetContent,
),
if (isArrangeActionButtonsVertical)
...[
if (_cancelText.isNotEmpty)
Expand Down
128 changes: 128 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,89 @@ 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);
}

// 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),
florentos17 marked this conversation as resolved.
Show resolved Hide resolved
domain: domain
).asString();
}
}

@override
String toString() {
return '$localPart@${domain.asString()}';
Expand Down Expand Up @@ -323,6 +414,10 @@ class MailAddress with EquatableMixin {
lpSB.write('.');
pos++;
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 == '@') {
// End of local-part
break;
Expand Down Expand Up @@ -416,6 +511,39 @@ 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();
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
List<Object?> get props => [localPart, domain];
}
48 changes: 48 additions & 0 deletions core/test/utils/mail_address_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ void main() {
"[email protected]",
"user+mailbox/[email protected]",
"[email protected]",
"[email protected]",
"user+my [email protected]",
"user+Dossier d'été@domain.com",
"\"Abc@def\"@example.com",
"\"Fred Bloggs\"@example.com",
"\"Joe.\\Blow\"@example.com",
Expand Down Expand Up @@ -56,6 +59,10 @@ void main() {
"server-dev@#123.apache.org",
"server-dev@[127.0.1.1.1]",
"server-dev@[127.0.1.-1]",
"[email protected]",
"user+ @domain.com",
"user+#[email protected]",
"user+test-_.!~*'() @domain.com",
"\"a..b\"@domain.com", // jakarta.mail is unable to handle this so we better reject it
"server-dev\\[email protected]", // jakarta.mail is unable to handle this so we better reject it
"[email protected]",
Expand Down Expand Up @@ -165,5 +172,46 @@ void main() {
final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS);
expect(mailAddress.toString(), equals(GOOD_ADDRESS));
});

test('MailAddress.encodeLocalPartDetails() should work with characters to encode', () {
final mailAddress = MailAddress.validateAddress("user+my [email protected]");
expect(mailAddress.asEncodedString(), 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.asEncodedString(), equals("user+Dossier%20d%27%C3%A9t%C3%[email protected]"));
});

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%[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]"));
});

});
}
1 change: 1 addition & 0 deletions integration_test/utils/scenario_utils_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ mixin ScenarioUtilsMixin {
toRecipients: {EmailAddress(null, provisioningEmail.toEmail)},
ccRecipients: {},
bccRecipients: {},
replyToRecipients: {},
outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId,
sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent],
identity: identity,
Expand Down
Loading
Loading