TF-3349 Escape messages when forward and reply email
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
extension HtmlExtension on String {
|
extension HtmlExtension on String {
|
||||||
|
|
||||||
static const String editorStartTags = '<div><br><br></div>';
|
static const String editorStartTags = '<div><br><br></div>';
|
||||||
@@ -40,4 +42,19 @@ extension HtmlExtension on String {
|
|||||||
'cite',
|
'cite',
|
||||||
attribute: 'style="text-align: left;display: block;"'
|
attribute: 'style="text-align: left;display: block;"'
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HtmlNullableExtension on String? {
|
||||||
|
String escapeHtmlString({HtmlEscapeMode escapeMode = HtmlEscapeMode.unknown}) {
|
||||||
|
try {
|
||||||
|
if (this?.trim().isNotEmpty != true) return '';
|
||||||
|
|
||||||
|
return HtmlEscape(escapeMode).convert(this!);
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String escapeLtGtHtmlString() =>
|
||||||
|
escapeHtmlString(escapeMode: HtmlEscapeMode.element);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:core/presentation/extensions/html_extension.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('HtmlNullableExtension::', () {
|
||||||
|
test('escapeHtmlString should escapes HTML containing onclick attribute', () {
|
||||||
|
String? input = "<a onclick=\"alert('hi')\">Click</a>";
|
||||||
|
expect(input.escapeHtmlString(),
|
||||||
|
'<a onclick="alert('hi')">Click</a>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapeHtmlString should escapes HTML containing multiple events', () {
|
||||||
|
String? input = "<div onmouseover=\"alert('hover')\" onclick=\"doSomething()\">Hover</div>";
|
||||||
|
expect(input.escapeHtmlString(),
|
||||||
|
'<div onmouseover="alert('hover')" onclick="doSomething()">Hover</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapeLtGtHtmlString should escapes only < and > with events', () {
|
||||||
|
String? input = "<button onclick=\"run()\">Run</button>";
|
||||||
|
expect(input.escapeLtGtHtmlString(),
|
||||||
|
'<button onclick=\"run()\">Run</button>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapeHtmlString should handles HTML with empty event attributes', () {
|
||||||
|
String? input = "<img src=\"image.png\" onerror=\"\">";
|
||||||
|
expect(input.escapeHtmlString(),
|
||||||
|
'<img src="image.png" onerror="">');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapeHtmlString should handles HTML with invalid syntax in events', () {
|
||||||
|
String? input = "<a onclick=\"alert('unclosed event)>Click</a>";
|
||||||
|
expect(input.escapeHtmlString(),
|
||||||
|
'<a onclick="alert('unclosed event)>Click</a>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -53,62 +53,63 @@ extension EmailActionTypeExtension on EmailActionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? getHeaderEmailQuoted({
|
String? getHeaderEmailQuoted({
|
||||||
required BuildContext context,
|
required Locale locale,
|
||||||
|
required AppLocalizations appLocalizations,
|
||||||
required PresentationEmail presentationEmail
|
required PresentationEmail presentationEmail
|
||||||
}) {
|
}) {
|
||||||
final locale = Localizations.localeOf(context).toLanguageTag();
|
final languageTag = locale.toLanguageTag();
|
||||||
switch(this) {
|
switch(this) {
|
||||||
case EmailActionType.reply:
|
case EmailActionType.reply:
|
||||||
case EmailActionType.replyAll:
|
case EmailActionType.replyAll:
|
||||||
final receivedAt = presentationEmail.receivedAt;
|
final receivedAt = presentationEmail.receivedAt;
|
||||||
final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true);
|
final emailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator();
|
||||||
return AppLocalizations.of(context).header_email_quoted(
|
return appLocalizations.header_email_quoted(
|
||||||
receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale),
|
receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: languageTag),
|
||||||
emailAddress
|
emailAddress
|
||||||
);
|
);
|
||||||
case EmailActionType.forward:
|
case EmailActionType.forward:
|
||||||
var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag();
|
var headerQuoted = '------- ${appLocalizations.forwarded_message} -------'.addNewLineTag();
|
||||||
|
|
||||||
final subject = presentationEmail.subject ?? '';
|
final subject = presentationEmail.subject?.escapeLtGtHtmlString() ?? '';
|
||||||
final receivedAt = presentationEmail.receivedAt;
|
final receivedAt = presentationEmail.receivedAt;
|
||||||
final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true);
|
final fromEmailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator();
|
||||||
final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true);
|
final toEmailAddress = presentationEmail.to.toEscapeHtmlStringUseCommaSeparator();
|
||||||
final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true);
|
final ccEmailAddress = presentationEmail.cc.toEscapeHtmlStringUseCommaSeparator();
|
||||||
final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true);
|
final bccEmailAddress = presentationEmail.bcc.toEscapeHtmlStringUseCommaSeparator();
|
||||||
|
|
||||||
if (subject.isNotEmpty) {
|
if (subject.isNotEmpty) {
|
||||||
headerQuoted = headerQuoted
|
headerQuoted = headerQuoted
|
||||||
.append('${AppLocalizations.of(context).subject_email}: ')
|
.append('${appLocalizations.subject_email}: ')
|
||||||
.append(subject)
|
.append(subject)
|
||||||
.addNewLineTag();
|
.addNewLineTag();
|
||||||
}
|
}
|
||||||
if (receivedAt != null) {
|
if (receivedAt != null) {
|
||||||
headerQuoted = headerQuoted
|
headerQuoted = headerQuoted
|
||||||
.append('${AppLocalizations.of(context).date}: ')
|
.append('${appLocalizations.date}: ')
|
||||||
.append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale))
|
.append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: languageTag))
|
||||||
.addNewLineTag();
|
.addNewLineTag();
|
||||||
}
|
}
|
||||||
if (fromEmailAddress.isNotEmpty) {
|
if (fromEmailAddress.isNotEmpty) {
|
||||||
headerQuoted = headerQuoted
|
headerQuoted = headerQuoted
|
||||||
.append('${AppLocalizations.of(context).from_email_address_prefix}: ')
|
.append('${appLocalizations.from_email_address_prefix}: ')
|
||||||
.append(fromEmailAddress)
|
.append(fromEmailAddress)
|
||||||
.addNewLineTag();
|
.addNewLineTag();
|
||||||
}
|
}
|
||||||
if (toEmailAddress.isNotEmpty) {
|
if (toEmailAddress.isNotEmpty) {
|
||||||
headerQuoted = headerQuoted
|
headerQuoted = headerQuoted
|
||||||
.append('${AppLocalizations.of(context).to_email_address_prefix}: ')
|
.append('${appLocalizations.to_email_address_prefix}: ')
|
||||||
.append(toEmailAddress)
|
.append(toEmailAddress)
|
||||||
.addNewLineTag();
|
.addNewLineTag();
|
||||||
}
|
}
|
||||||
if (ccEmailAddress.isNotEmpty) {
|
if (ccEmailAddress.isNotEmpty) {
|
||||||
headerQuoted = headerQuoted
|
headerQuoted = headerQuoted
|
||||||
.append('${AppLocalizations.of(context).cc_email_address_prefix}: ')
|
.append('${appLocalizations.cc_email_address_prefix}: ')
|
||||||
.append(ccEmailAddress)
|
.append(ccEmailAddress)
|
||||||
.addNewLineTag();
|
.addNewLineTag();
|
||||||
}
|
}
|
||||||
if (bccEmailAddress.isNotEmpty) {
|
if (bccEmailAddress.isNotEmpty) {
|
||||||
headerQuoted = headerQuoted
|
headerQuoted = headerQuoted
|
||||||
.append('${AppLocalizations.of(context).bcc_email_address_prefix}: ')
|
.append('${appLocalizations.bcc_email_address_prefix}: ')
|
||||||
.append(bccEmailAddress)
|
.append(bccEmailAddress)
|
||||||
.addNewLineTag();
|
.addNewLineTag();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:model/email/email_action_type.dart';
|
import 'package:model/email/email_action_type.dart';
|
||||||
import 'package:model/email/presentation_email.dart';
|
import 'package:model/email/presentation_email.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
|
||||||
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
|
|
||||||
mixin EditorViewMixin {
|
mixin EditorViewMixin {
|
||||||
String getEmailContentQuotedAsHtml({
|
String getEmailContentQuotedAsHtml({
|
||||||
required BuildContext context,
|
required Locale locale,
|
||||||
|
required AppLocalizations appLocalizations,
|
||||||
required String emailContent,
|
required String emailContent,
|
||||||
required EmailActionType emailActionType,
|
required EmailActionType emailActionType,
|
||||||
required PresentationEmail presentationEmail,
|
required PresentationEmail presentationEmail,
|
||||||
}) {
|
}) {
|
||||||
final headerEmailQuoted = emailActionType.getHeaderEmailQuoted(
|
final headerEmailQuoted = emailActionType.getHeaderEmailQuoted(
|
||||||
context: context,
|
locale: locale,
|
||||||
|
appLocalizations: appLocalizations,
|
||||||
presentationEmail: presentationEmail
|
presentationEmail: presentationEmail
|
||||||
);
|
);
|
||||||
log('EditorViewMixin::getEmailContentQuotedAsHtml:headerEmailQuoted: $headerEmailQuoted');
|
log('EditorViewMixin::getEmailContentQuotedAsHtml:headerEmailQuoted: $headerEmailQuoted');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/mobi
|
|||||||
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
|
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
|
||||||
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
|
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
||||||
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
||||||
|
|
||||||
class MobileEditorView extends StatelessWidget with EditorViewMixin {
|
class MobileEditorView extends StatelessWidget with EditorViewMixin {
|
||||||
@@ -87,7 +88,8 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin {
|
|||||||
return contentViewState!.fold(
|
return contentViewState!.fold(
|
||||||
(failure) {
|
(failure) {
|
||||||
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
||||||
context: context,
|
locale: Localizations.localeOf(context),
|
||||||
|
appLocalizations: AppLocalizations.of(context),
|
||||||
emailContent: '',
|
emailContent: '',
|
||||||
emailActionType: arguments!.emailActionType,
|
emailActionType: arguments!.emailActionType,
|
||||||
presentationEmail: arguments!.presentationEmail!
|
presentationEmail: arguments!.presentationEmail!
|
||||||
@@ -104,7 +106,8 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin {
|
|||||||
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
|
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
|
||||||
} else {
|
} else {
|
||||||
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
||||||
context: context,
|
locale: Localizations.localeOf(context),
|
||||||
|
appLocalizations: AppLocalizations.of(context),
|
||||||
emailContent: success is TransformHtmlEmailContentSuccess
|
emailContent: success is TransformHtmlEmailContentSuccess
|
||||||
? success.htmlContent
|
? success.htmlContent
|
||||||
: '',
|
: '',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/web/web_edi
|
|||||||
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
|
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
|
||||||
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
|
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
||||||
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
||||||
|
|
||||||
class WebEditorView extends StatelessWidget with EditorViewMixin {
|
class WebEditorView extends StatelessWidget with EditorViewMixin {
|
||||||
@@ -152,7 +153,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
|
|||||||
return contentViewState!.fold(
|
return contentViewState!.fold(
|
||||||
(failure) {
|
(failure) {
|
||||||
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
||||||
context: context,
|
locale: Localizations.localeOf(context),
|
||||||
|
appLocalizations: AppLocalizations.of(context),
|
||||||
emailContent: '',
|
emailContent: '',
|
||||||
emailActionType: arguments!.emailActionType,
|
emailActionType: arguments!.emailActionType,
|
||||||
presentationEmail: arguments!.presentationEmail!
|
presentationEmail: arguments!.presentationEmail!
|
||||||
@@ -181,7 +183,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
|
|||||||
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
|
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
|
||||||
} else {
|
} else {
|
||||||
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
final emailContentQuoted = getEmailContentQuotedAsHtml(
|
||||||
context: context,
|
locale: Localizations.localeOf(context),
|
||||||
|
appLocalizations: AppLocalizations.of(context),
|
||||||
emailContent: success is TransformHtmlEmailContentSuccess
|
emailContent: success is TransformHtmlEmailContentSuccess
|
||||||
? success.htmlContent
|
? success.htmlContent
|
||||||
: '',
|
: '',
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ extension EmailAddressExtension on EmailAddress {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String asFullStringWithLtGtCharacter() {
|
||||||
|
if (displayName.isNotEmpty && emailAddress.isNotEmpty) {
|
||||||
|
return '${displayName.capitalizeFirstEach} <$emailAddress>';
|
||||||
|
} else if (displayName.isNotEmpty) {
|
||||||
|
return displayName.capitalizeFirstEach;
|
||||||
|
} else if (emailAddress.isNotEmpty) {
|
||||||
|
return '<$emailAddress>';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
String get emailAddress => email ?? '';
|
String get emailAddress => email ?? '';
|
||||||
|
|
||||||
String get displayName => name ?? '';
|
String get displayName => name ?? '';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:core/presentation/extensions/html_extension.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
import 'package:model/mailbox/expand_mode.dart';
|
import 'package:model/mailbox/expand_mode.dart';
|
||||||
import 'package:model/extensions/email_address_extension.dart';
|
import 'package:model/extensions/email_address_extension.dart';
|
||||||
@@ -27,6 +28,18 @@ extension SetEmailAddressExtension on Set<EmailAddress>? {
|
|||||||
return listEmail.isNotEmpty ? listEmail.join(', ') : '';
|
return listEmail.isNotEmpty ? listEmail.join(', ') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String toEscapeHtmlString(String separator) {
|
||||||
|
if (this?.isNotEmpty != true) return '';
|
||||||
|
|
||||||
|
final listEmail = this
|
||||||
|
!.map((emailAddress) => emailAddress.asFullStringWithLtGtCharacter().escapeLtGtHtmlString())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return listEmail.isNotEmpty ? listEmail.join(separator) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
String toEscapeHtmlStringUseCommaSeparator() => toEscapeHtmlString(', ');
|
||||||
|
|
||||||
int numberEmailAddress() => this != null ? this!.length : 0;
|
int numberEmailAddress() => this != null ? this!.length : 0;
|
||||||
|
|
||||||
List<EmailAddress> filterEmailAddress(String emailAddressNotExist) {
|
List<EmailAddress> filterEmailAddress(String emailAddressNotExist) {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
|
import 'package:model/extensions/email_address_extension.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('EmailAddressExtension::asFullStringWithLtGtCharacter::', () {
|
||||||
|
test('Should returns displayName and emailAddress formatted correctly', () {
|
||||||
|
final emailAddress = EmailAddress(
|
||||||
|
'john doe',
|
||||||
|
'john.doe@example.com',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
emailAddress.asFullStringWithLtGtCharacter(),
|
||||||
|
'John Doe <john.doe@example.com>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should returns displayName capitalized when emailAddress is empty', () {
|
||||||
|
final emailAddress = EmailAddress(
|
||||||
|
'jane doe',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
expect(emailAddress.asFullStringWithLtGtCharacter(), 'Jane Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should returns emailAddress enclosed in <> when displayName is empty', () {
|
||||||
|
final emailAddress = EmailAddress(
|
||||||
|
'',
|
||||||
|
'jane.doe@example.com',
|
||||||
|
);
|
||||||
|
expect(emailAddress.asFullStringWithLtGtCharacter(), '<jane.doe@example.com>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should returns an empty string when both displayName and emailAddress are empty', () {
|
||||||
|
final emailAddress = EmailAddress('', '');
|
||||||
|
expect(emailAddress.asFullStringWithLtGtCharacter(), '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
|
import 'package:model/extensions/list_email_address_extension.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ListEmailAddressExtension::toEscapeHtmlString::', () {
|
||||||
|
test('Should returns empty string for null or empty list', () {
|
||||||
|
expect((<EmailAddress>{}).toEscapeHtmlString(', '), '');
|
||||||
|
|
||||||
|
Set<EmailAddress>? listEmails;
|
||||||
|
expect(listEmails.toEscapeHtmlString(', '), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should returns joined HTML strings with separator', () {
|
||||||
|
final emails = {
|
||||||
|
EmailAddress('John Doe', 'john@example.com'),
|
||||||
|
EmailAddress('Jane Smith', 'jane@example.com'),
|
||||||
|
};
|
||||||
|
final result = emails.toEscapeHtmlString(' | ');
|
||||||
|
|
||||||
|
expect(result, 'John Doe <john@example.com> | Jane Smith <jane@example.com>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should handles displayName-only or email-only cases', () {
|
||||||
|
final emails = {
|
||||||
|
EmailAddress('John Doe', ''),
|
||||||
|
EmailAddress('', 'jane@example.com'),
|
||||||
|
};
|
||||||
|
final result = emails.toEscapeHtmlString(', ');
|
||||||
|
|
||||||
|
expect(result, 'John Doe, <jane@example.com>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ListEmailAddressExtension::toEscapeHtmlStringUseCommaSeparator::', () {
|
||||||
|
test('Should uses ", " as separator', () {
|
||||||
|
final emails = {
|
||||||
|
EmailAddress('John Doe', 'john@example.com'),
|
||||||
|
EmailAddress('Jane Smith', 'jane@example.com'),
|
||||||
|
};
|
||||||
|
final result = emails.toEscapeHtmlStringUseCommaSeparator();
|
||||||
|
|
||||||
|
expect(result, 'John Doe <john@example.com>, Jane Smith <jane@example.com>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user