TF-2667 Validate email in recipient form before send email
This commit is contained in:
@@ -46,6 +46,8 @@ export 'utils/option_param_mixin.dart';
|
|||||||
export 'utils/print_utils.dart';
|
export 'utils/print_utils.dart';
|
||||||
export 'utils/broadcast_channel/broadcast_channel.dart';
|
export 'utils/broadcast_channel/broadcast_channel.dart';
|
||||||
export 'utils/list_utils.dart';
|
export 'utils/list_utils.dart';
|
||||||
|
export 'utils/mail/domain.dart';
|
||||||
|
export 'utils/mail/mail_address.dart';
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
export 'presentation/views/text/slogan_builder.dart';
|
export 'presentation/views/text/slogan_builder.dart';
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class AddressException with EquatableMixin implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
AddressException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [message];
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class Domain with EquatableMixin {
|
||||||
|
static final RegExp _dashMatcher = RegExp(r"[-_]");
|
||||||
|
static final RegExp _digitMatcher = RegExp(r"[0-9]");
|
||||||
|
static final RegExp _partCharMatcher = RegExp(r"[a-zA-Z0-9\-._]");
|
||||||
|
|
||||||
|
static final Domain localhost = Domain.of('localhost');
|
||||||
|
static const int maximumDomainLength = 253;
|
||||||
|
|
||||||
|
static String _removeBrackets(String domainName) {
|
||||||
|
if (!(domainName.startsWith("[") && domainName.endsWith("]"))) {
|
||||||
|
return domainName;
|
||||||
|
}
|
||||||
|
return domainName.substring(1, domainName.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Domain of(String domain) {
|
||||||
|
assert(
|
||||||
|
domain.length <= maximumDomainLength,
|
||||||
|
'Domain name length should not exceed $maximumDomainLength characters'
|
||||||
|
);
|
||||||
|
|
||||||
|
String domainWithoutBrackets = _removeBrackets(domain);
|
||||||
|
assert(
|
||||||
|
_partCharMatcher
|
||||||
|
.allMatches(domainWithoutBrackets)
|
||||||
|
.every((match) => match.group(0) != null),
|
||||||
|
'Domain parts ASCII chars must be a-z A-Z 0-9 - or _'
|
||||||
|
);
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
int nextDot = domainWithoutBrackets.indexOf('.');
|
||||||
|
|
||||||
|
while (nextDot > -1) {
|
||||||
|
if (pos + 1 > domainWithoutBrackets.length) {
|
||||||
|
throw ArgumentError('Last domain part should not be empty');
|
||||||
|
}
|
||||||
|
_assertValidPart(domainWithoutBrackets, pos, nextDot);
|
||||||
|
pos = nextDot + 1;
|
||||||
|
nextDot = domainWithoutBrackets.indexOf('.', pos);
|
||||||
|
}
|
||||||
|
_assertValidPart(domainWithoutBrackets, pos, domainWithoutBrackets.length);
|
||||||
|
_assertValidLastPart(domainWithoutBrackets, pos);
|
||||||
|
return Domain._(domainWithoutBrackets);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _assertValidPart(String domainPart, int begin, int end) {
|
||||||
|
assert(begin != end, "Domain part should not be empty");
|
||||||
|
assert(!_dashMatcher.hasMatch(domainPart[begin]),
|
||||||
|
"Domain part should not start with '-' or '_'");
|
||||||
|
assert(!_dashMatcher.hasMatch(domainPart[end - 1]),
|
||||||
|
"Domain part should not end with '-' or '_'");
|
||||||
|
assert(
|
||||||
|
end - begin <= 63, "Domain part should not not exceed 63 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _assertValidLastPart(String domainPart, int pos) {
|
||||||
|
bool onlyDigits = _digitMatcher.hasMatch(domainPart[pos]);
|
||||||
|
bool invalid = onlyDigits && !_validIPAddress(domainPart);
|
||||||
|
assert(!invalid, "The last domain part must not start with 0-9");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _validIPAddress(String value) {
|
||||||
|
try {
|
||||||
|
Uri.parseIPv6Address(value);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String domainName;
|
||||||
|
final String normalizedDomainName;
|
||||||
|
|
||||||
|
Domain._(this.domainName)
|
||||||
|
: normalizedDomainName = _removeBrackets(domainName.toLowerCase());
|
||||||
|
|
||||||
|
String name() {
|
||||||
|
return domainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String asString() {
|
||||||
|
return normalizedDomainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is Domain) {
|
||||||
|
return normalizedDomainName == other.normalizedDomainName;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return normalizedDomainName.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "Domain : $domainName";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [domainName];
|
||||||
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
import 'package:core/domain/exceptions/address_exception.dart';
|
||||||
|
import 'package:core/utils/app_logger.dart';
|
||||||
|
import 'package:core/utils/mail/domain.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class MailAddress with EquatableMixin {
|
||||||
|
static final List<String> SPECIAL = [
|
||||||
|
'<',
|
||||||
|
'>',
|
||||||
|
'(',
|
||||||
|
')',
|
||||||
|
'[',
|
||||||
|
']',
|
||||||
|
'\\',
|
||||||
|
'.',
|
||||||
|
',',
|
||||||
|
';',
|
||||||
|
':',
|
||||||
|
'@',
|
||||||
|
'\"'
|
||||||
|
];
|
||||||
|
|
||||||
|
final String localPart;
|
||||||
|
final Domain domain;
|
||||||
|
|
||||||
|
MailAddress({required this.localPart, required this.domain});
|
||||||
|
|
||||||
|
String asString() {
|
||||||
|
return '$localPart@${domain.asString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String asPrettyString() {
|
||||||
|
return '<${asString()}>';
|
||||||
|
}
|
||||||
|
|
||||||
|
static MailAddress validate(String address) {
|
||||||
|
String localPart;
|
||||||
|
Domain domain;
|
||||||
|
|
||||||
|
address = address.trim();
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
// Test if mail address has source routing information (RFC-821) and get rid of it!!
|
||||||
|
// must be called first!! (or at least prior to updating pos)
|
||||||
|
_stripSourceRoute(address, pos);
|
||||||
|
|
||||||
|
StringBuffer localPartSB = StringBuffer();
|
||||||
|
StringBuffer domainSB = StringBuffer();
|
||||||
|
// Begin parsing
|
||||||
|
// <mailbox> ::= <local-part> "@" <domain>
|
||||||
|
|
||||||
|
try {
|
||||||
|
// parse local-part
|
||||||
|
// <local-part> ::= <dot-string> | <quoted-string>
|
||||||
|
if (address[pos] == '\"') {
|
||||||
|
pos = _parseQuotedLocalPartOrThrowException(localPartSB, address, pos);
|
||||||
|
} else {
|
||||||
|
pos = parseUnquotedLocalPartOrThrowException(localPartSB, address, pos);
|
||||||
|
}
|
||||||
|
// find @
|
||||||
|
if (pos >= address.length || address[pos] != '@') {
|
||||||
|
throw AddressException('Did not find @ between local-part and domain at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
// parse domain
|
||||||
|
// <domain> ::= <element> | <element> "." <domain>
|
||||||
|
// <element> ::= <name> | "#" <number> | "[" <dotnum> "]"
|
||||||
|
while (true) {
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var postChar = address[pos];
|
||||||
|
if (postChar == '#') {
|
||||||
|
pos = _parseNumber(domainSB, address, pos);
|
||||||
|
} else if (postChar == '[') {
|
||||||
|
pos = _parseDomainLiteral(domainSB, address, pos);
|
||||||
|
} else {
|
||||||
|
pos = _parseDomain(domainSB, address, pos);
|
||||||
|
}
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
postChar = address[pos];
|
||||||
|
if (postChar == '.') {
|
||||||
|
var lastChar = address[pos - 1];
|
||||||
|
if (lastChar == '@' || lastChar == '.') {
|
||||||
|
throw AddressException('Subdomain expected before "." or duplicate "." in "address"');
|
||||||
|
}
|
||||||
|
domainSB.write('.');
|
||||||
|
pos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (domainSB.length == 0) {
|
||||||
|
throw AddressException('No domain found at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('MailAddress::validate: Exception = $e');
|
||||||
|
if (e is AddressException) {
|
||||||
|
rethrow;
|
||||||
|
} else {
|
||||||
|
throw AddressException('Out of data at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localPart = localPartSB.toString();
|
||||||
|
|
||||||
|
if (localPart.startsWith('.') ||
|
||||||
|
localPart.endsWith('.') ||
|
||||||
|
_haveDoubleDot(localPart)) {
|
||||||
|
throw AddressException('Addresses cannot start end with "." or contain two consecutive dots');
|
||||||
|
}
|
||||||
|
|
||||||
|
domain = _createDomain(domainSB.toString());
|
||||||
|
|
||||||
|
log('MailAddress::validate: localPart = $localPart | domain = $domain');
|
||||||
|
|
||||||
|
return MailAddress(localPart: localPart, domain: domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _haveDoubleDot(String localPart) {
|
||||||
|
return localPart.contains('..');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Domain _createDomain(String domain) {
|
||||||
|
try {
|
||||||
|
return Domain.of(domain);
|
||||||
|
} catch (e) {
|
||||||
|
throw AddressException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseNumber(StringBuffer dSB, String address, int pos) {
|
||||||
|
// <number> ::= <d> | <d> <number>
|
||||||
|
|
||||||
|
// we were passed the string with pos pointing the the # char.
|
||||||
|
// take the first char (#), put it in the result buffer and increment pos
|
||||||
|
var postChar = address[pos];
|
||||||
|
dSB.write(postChar);
|
||||||
|
pos++;
|
||||||
|
// We keep the position from the class level pos field
|
||||||
|
while (true) {
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// <d> ::= any one of the ten digits 0 through 9
|
||||||
|
var d = address[pos];
|
||||||
|
if (d == '.') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (d.compareTo('0') < 0 || d.compareTo('9') > 0) {
|
||||||
|
throw AddressException('In domain, did not find a number in # address at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
dSB.write(d);
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (dSB.length < 2) {
|
||||||
|
throw AddressException('In domain, did not find a number in # address at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseDomainLiteral(StringBuffer dSB, String address, int pos) {
|
||||||
|
// we were passed the string with pos pointing the the [ char.
|
||||||
|
// take the first char ([), put it in the result buffer and increment pos
|
||||||
|
var posChar = address[pos];
|
||||||
|
dSB.write(posChar);
|
||||||
|
pos++;
|
||||||
|
|
||||||
|
// <dotnum> ::= <snum> "." <snum> "." <snum> "." <snum>
|
||||||
|
for (int octet = 0; octet < 4; octet++) {
|
||||||
|
// <snum> ::= one, two, or three digits representing a decimal
|
||||||
|
// integer value in the range 0 through 255
|
||||||
|
// <d> ::= any one of the ten digits 0 through 9
|
||||||
|
StringBuffer snumSB = StringBuffer();
|
||||||
|
for (int digits = 0; digits < 3; digits++) {
|
||||||
|
String currentChar = address[pos];
|
||||||
|
if (currentChar == '.' || currentChar == ']') {
|
||||||
|
break;
|
||||||
|
} else if (currentChar.compareTo('0') < 0 ||
|
||||||
|
currentChar.compareTo('9') > 0) {
|
||||||
|
throw AddressException('Invalid number at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
snumSB.write(currentChar);
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (snumSB.length == 0) {
|
||||||
|
throw AddressException('Number not found at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int snum = int.parse(snumSB.toString());
|
||||||
|
if (snum > 255) {
|
||||||
|
throw AddressException('Invalid number at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw AddressException('Invalid number at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
dSB.write(snumSB.toString());
|
||||||
|
var posChar = address[pos];
|
||||||
|
if (posChar == ']') {
|
||||||
|
if (octet < 3) {
|
||||||
|
throw AddressException('End of number reached too quickly at ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (posChar == '.') {
|
||||||
|
dSB.write('.');
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
posChar = address[pos];
|
||||||
|
if (posChar != ']') {
|
||||||
|
throw AddressException('Did not find closing bracket \"]\" in domain at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
dSB.write(']');
|
||||||
|
pos++;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseDomain(StringBuffer dSB, String address, int pos) {
|
||||||
|
StringBuffer resultSB = StringBuffer();
|
||||||
|
// <name> ::= <a> <ldh-str> <let-dig>
|
||||||
|
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||||
|
// <let-dig> ::= <a> | <d>
|
||||||
|
// <let-dig-hyp> ::= <a> | <d> | "-"
|
||||||
|
// <a> ::= any one of the 52 alphabetic characters A through Z
|
||||||
|
// in upper case and a through z in lower case
|
||||||
|
// <d> ::= any one of the ten digits 0 through 9
|
||||||
|
|
||||||
|
// basically, this is a series of letters, digits, and hyphens,
|
||||||
|
// but it can't start with a digit or hypthen
|
||||||
|
// and can't end with a hyphen
|
||||||
|
|
||||||
|
// in practice though, we should relax this as domain names can start
|
||||||
|
// with digits as well as letters. So only check that doesn't start
|
||||||
|
// or end with hyphen.
|
||||||
|
while (true) {
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var ch = address[pos];
|
||||||
|
if ((ch.compareTo('0') >= 0 && ch.compareTo('9') <= 0) ||
|
||||||
|
(ch.compareTo('a') >= 0 && ch.compareTo('z') <= 0) ||
|
||||||
|
(ch.compareTo('A') >= 0 && ch.compareTo('Z') <= 0) ||
|
||||||
|
(ch == '-')) {
|
||||||
|
resultSB.write(ch);
|
||||||
|
pos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '.') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw AddressException('Invalid character at $pos in "$address"');
|
||||||
|
}
|
||||||
|
String result = resultSB.toString();
|
||||||
|
if (result.startsWith('-') || result.endsWith('-')) {
|
||||||
|
throw AddressException('Domain name cannot begin or end with a hyphen \"-\" at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
dSB.write(result);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _stripSourceRoute(String address, int pos) {
|
||||||
|
var posChar = address[pos];
|
||||||
|
if (pos < address.length && posChar == '@') {
|
||||||
|
int i = address.indexOf(':');
|
||||||
|
if (i != -1) {
|
||||||
|
pos = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseUnquotedLocalPart(StringBuffer lpSB, String address, int pos) {
|
||||||
|
// <dot-string> ::= <string> | <string> "." <dot-string>
|
||||||
|
bool lastCharDot = false;
|
||||||
|
while (true) {
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// <string> ::= <char> | <char> <string>
|
||||||
|
// <char> ::= <c> | "\" <x>
|
||||||
|
var postChar = address[pos];
|
||||||
|
if (postChar == '\\') {
|
||||||
|
lpSB.write('\\');
|
||||||
|
pos++;
|
||||||
|
// <x> ::= any one of the 128 ASCII characters (no exceptions)
|
||||||
|
var x = address[pos];
|
||||||
|
if (x.codeUnitAt(0) < 0 || x.codeUnitAt(0) > 127) {
|
||||||
|
throw AddressException('Invalid \\ syntax character at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
lpSB.write(x);
|
||||||
|
pos++;
|
||||||
|
lastCharDot = false;
|
||||||
|
} else if (postChar == '.') {
|
||||||
|
if (pos == 0) {
|
||||||
|
throw AddressException('Local part must not start with a "."');
|
||||||
|
}
|
||||||
|
lpSB.write('.');
|
||||||
|
pos++;
|
||||||
|
lastCharDot = true;
|
||||||
|
} else if (postChar == '@') {
|
||||||
|
// End of local-part
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// <c> ::= any one of the 128 ASCII characters, but not any
|
||||||
|
// <special> or <SP>
|
||||||
|
// <special> ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "."
|
||||||
|
// | "," | ";" | ":" | "@" """ | the control
|
||||||
|
// characters (ASCII codes 0 through 31 inclusive and
|
||||||
|
// 127)
|
||||||
|
// <SP> ::= the space character (ASCII code 32)
|
||||||
|
var c = address[pos];
|
||||||
|
if (c.codeUnitAt(0) <= 31 || c.codeUnitAt(0) >= 127 || c == ' ') {
|
||||||
|
throw AddressException('Invalid character in local-part (user account) at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
while (i < SPECIAL.length) {
|
||||||
|
if (c == SPECIAL[i]) {
|
||||||
|
throw AddressException('Invalid character in local-part (user account) at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
lpSB.write(c);
|
||||||
|
pos++;
|
||||||
|
lastCharDot = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastCharDot) {
|
||||||
|
throw AddressException('local-part (user account) ended with a \".\", which is invalid in address "$address"');
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parseUnquotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos) {
|
||||||
|
pos = _parseUnquotedLocalPart(localPartSB, address, pos);
|
||||||
|
if (localPartSB.length == 0) {
|
||||||
|
throw AddressException('No local-part (user account) found at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseQuotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos) {
|
||||||
|
pos = _parseQuotedLocalPart(localPartSB, address, pos);
|
||||||
|
if (localPartSB.length == 2) {
|
||||||
|
throw AddressException('No quoted local-part (user account) found at position ${pos + 2} in "$address"');
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseQuotedLocalPart(StringBuffer lpSB, String address, int pos) {
|
||||||
|
lpSB.write('\"');
|
||||||
|
pos++;
|
||||||
|
// <quoted-string> ::= """ <qtext> """
|
||||||
|
// <qtext> ::= "\" <x> | "\" <x> <qtext> | <q> | <q> <qtext>
|
||||||
|
while (true) {
|
||||||
|
if (pos >= address.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var postChar = address[pos];
|
||||||
|
if (postChar == '\"') {
|
||||||
|
lpSB.write('\"');
|
||||||
|
// end of quoted string... move forward
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (postChar == '\\') {
|
||||||
|
lpSB.write('\\');
|
||||||
|
pos++;
|
||||||
|
// <x> ::= any one of the 128 ASCII characters (no exceptions)
|
||||||
|
var x = address[pos];
|
||||||
|
if (x.codeUnitAt(0) < 0 || x.codeUnitAt(0) > 127) {
|
||||||
|
throw AddressException('Invalid \\ syntax character at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
lpSB.write(x);
|
||||||
|
pos++;
|
||||||
|
} else {
|
||||||
|
// <q> ::= any one of the 128 ASCII characters except <CR>,
|
||||||
|
// <LF>, quote ("), or backslash (\)
|
||||||
|
var q = address[pos];
|
||||||
|
if (q.codeUnitAt(0) <= 0 ||
|
||||||
|
q == '\n' ||
|
||||||
|
q == '\r' ||
|
||||||
|
q == '\"' ||
|
||||||
|
q == '\\') {
|
||||||
|
throw AddressException('Unquoted local-part (user account) must be one of the 128 ASCII characters exception <CR>, <LF>, quote (\"), or backslash (\\) at position ${pos + 1} in "$address"');
|
||||||
|
}
|
||||||
|
lpSB.write(q);
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [localPart, domain];
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:core/domain/exceptions/address_exception.dart';
|
||||||
|
import 'package:core/utils/mail/mail_address.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('MailAddress test', () {
|
||||||
|
test('validate method should be return MailAddress when address valid', () {
|
||||||
|
String validAddress = 'user@example.com';
|
||||||
|
MailAddress mailAddress = MailAddress.validate(validAddress);
|
||||||
|
expect(mailAddress.localPart, equals('user'));
|
||||||
|
expect(mailAddress.domain.name(), equals('example.com'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate method should be throw AddressException when address missing @', () {
|
||||||
|
String invalidAddress = 'userexample.com';
|
||||||
|
expect(
|
||||||
|
() => MailAddress.validate(invalidAddress),
|
||||||
|
throwsA(isA<AddressException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
'Did not find @ between local-part and domain at position 16 in "userexample.com"')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate method should be throw AddressException when address empty local-part', () {
|
||||||
|
String invalidAddress = '@example.com';
|
||||||
|
expect(
|
||||||
|
() => MailAddress.validate(invalidAddress),
|
||||||
|
throwsA(isA<AddressException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
'No local-part (user account) found at position 1 in "@example.com"')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate method should be throw AddressException when address empty domain', () {
|
||||||
|
String invalidAddress = 'user@';
|
||||||
|
expect(
|
||||||
|
() => MailAddress.validate(invalidAddress),
|
||||||
|
throwsA(isA<AddressException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
'No domain found at position 6 in "user@"')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate method should be throw AddressException when address with a hyphen "-"', () {
|
||||||
|
String invalidAddress = 'user@-example.com';
|
||||||
|
expect(
|
||||||
|
() => MailAddress.validate(invalidAddress),
|
||||||
|
throwsA(isA<AddressException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
'Domain name cannot begin or end with a hyphen "-" at position 14 in "user@-example.com"')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_c
|
|||||||
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
|
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart';
|
import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.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/features/email/presentation/utils/email_utils.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart';
|
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
|
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart';
|
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart';
|
||||||
@@ -721,7 +722,7 @@ class ComposerController extends BaseController with DragDropFileMixin {
|
|||||||
|
|
||||||
final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress;
|
final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress;
|
||||||
final listEmailAddressInvalid = allListEmailAddress
|
final listEmailAddressInvalid = allListEmailAddress
|
||||||
.where((emailAddress) => !GetUtils.isEmail(emailAddress.emailAddress))
|
.where((emailAddress) => !EmailUtils.isEmailAddressValid(emailAddress.emailAddress))
|
||||||
.toList();
|
.toList();
|
||||||
if (listEmailAddressInvalid.isNotEmpty) {
|
if (listEmailAddressInvalid.isNotEmpty) {
|
||||||
showConfirmDialogAction(context,
|
showConfirmDialogAction(context,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/composer/presentation/model/draggable_ema
|
|||||||
import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||||
|
|
||||||
class RecipientTagItemWidget extends StatelessWidget {
|
class RecipientTagItemWidget extends StatelessWidget {
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ class RecipientTagItemWidget extends StatelessWidget {
|
|||||||
Color _getTagBackgroundColor() {
|
Color _getTagBackgroundColor() {
|
||||||
if (isLatestTagFocused && isLatestEmail) {
|
if (isLatestTagFocused && isLatestEmail) {
|
||||||
return AppColor.colorItemRecipientSelected;
|
return AppColor.colorItemRecipientSelected;
|
||||||
} else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) {
|
} else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) {
|
||||||
return AppColor.colorEmailAddressTag;
|
return AppColor.colorEmailAddressTag;
|
||||||
} else {
|
} else {
|
||||||
return Colors.white;
|
return Colors.white;
|
||||||
@@ -182,7 +183,7 @@ class RecipientTagItemWidget extends StatelessWidget {
|
|||||||
BorderSide _getTagBorderSide() {
|
BorderSide _getTagBorderSide() {
|
||||||
if (isLatestTagFocused && isLatestEmail) {
|
if (isLatestTagFocused && isLatestEmail) {
|
||||||
return const BorderSide(width: 1, color: AppColor.primaryColor);
|
return const BorderSide(width: 1, color: AppColor.primaryColor);
|
||||||
} else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) {
|
} else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) {
|
||||||
return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag);
|
return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag);
|
||||||
} else {
|
} else {
|
||||||
return const BorderSide(
|
return const BorderSide(
|
||||||
|
|||||||
@@ -68,7 +68,16 @@ class EmailUtils {
|
|||||||
required String internalDomain
|
required String internalDomain
|
||||||
}) {
|
}) {
|
||||||
log('EmailUtils::isSameDomain: emailAddress = $emailAddress | internalDomain = $internalDomain');
|
log('EmailUtils::isSameDomain: emailAddress = $emailAddress | internalDomain = $internalDomain');
|
||||||
return GetUtils.isEmail(emailAddress) &&
|
return EmailUtils.isEmailAddressValid(emailAddress) &&
|
||||||
emailAddress.split('@').last.toLowerCase() == internalDomain.toLowerCase();
|
emailAddress.split('@').last.toLowerCase() == internalDomain.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool isEmailAddressValid(String address) {
|
||||||
|
try {
|
||||||
|
return GetUtils.isEmail(address) && MailAddress.validate(address).asString().isNotEmpty;
|
||||||
|
} catch(e) {
|
||||||
|
logError('EmailUtils::isEmailAddressValid: Exception = $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user