email_integrity 0.1.0
email_integrity: ^0.1.0 copied to clipboard
A production-ready Flutter/Dart package for email validation, syntax checks, normalization, disposable & fake email detection, custom domain rules, and DNS/MX validation.
import 'package:email_integrity/email_integrity.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const EmailIntegrityExampleApp());
}
class EmailIntegrityExampleApp extends StatelessWidget {
const EmailIntegrityExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Email Integrity Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.light,
),
useMaterial3: true,
),
home: const MainHomeScreen(),
);
}
}
class MainHomeScreen extends StatelessWidget {
const MainHomeScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.shield_outlined, color: Colors.deepPurple),
SizedBox(width: 8),
Text('Email Integrity', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
centerTitle: true,
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.analytics_outlined), text: 'Inspector'),
Tab(icon: Icon(Icons.category_outlined), text: 'Classifier'),
Tab(icon: Icon(Icons.dynamic_form_outlined), text: 'Form Demo'),
],
),
),
body: const TabBarView(
children: [
EmailInspectorTab(),
EmailClassifierTab(),
FormValidationTab(),
],
),
),
);
}
}
// ============================================================================
// TAB 1: EMAIL INSPECTOR & VALIDATOR
// ============================================================================
class EmailInspectorTab extends StatefulWidget {
const EmailInspectorTab({super.key});
@override
State<EmailInspectorTab> createState() => _EmailInspectorTabState();
}
class _EmailInspectorTabState extends State<EmailInspectorTab> {
final _emailController = TextEditingController(text: 'user@gmail.com');
final _allowedDomainsController = TextEditingController();
final _blockedDomainsController = TextEditingController();
EmailValidationMode _mode = EmailValidationMode.standard;
DomainPolicy _policy = DomainPolicy.allowAny;
bool _checkDisposable = true;
bool _checkFakeDomains = true;
bool _checkDns = false;
bool _checkMx = false;
bool _applyProviderNormalization = false;
bool _isLoading = false;
EmailValidationResult? _result;
final List<String> _quickTestEmails = [
'user@gmail.com',
'user@proton.me',
'employee@acme.com',
'user@startup.app',
'test@mailinator.com',
'test@example.com',
'user@münchen.de',
'user@@company.com',
];
Future<void> _runValidation() async {
setState(() => _isLoading = true);
final allowed = _allowedDomainsController.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet();
final blocked = _blockedDomainsController.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet();
final options = EmailValidationOptions(
mode: _mode,
domainPolicy: _policy,
checkDisposable: _checkDisposable,
checkFakeDomains: _checkFakeDomains,
checkDns: _checkDns,
checkMx: _checkMx,
applyProviderNormalization: _applyProviderNormalization,
allowedDomains: allowed,
blockedDomains: blocked,
);
final res = await EmailIntegrity.validate(
_emailController.text,
options: options,
);
setState(() {
_isLoading = false;
_result = res;
});
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16.0),
children: [
// Quick Presets
const Text(
'Quick Test Presets',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 4,
children: _quickTestEmails.map((email) {
return ActionChip(
label: Text(email, style: const TextStyle(fontSize: 12)),
onPressed: () {
_emailController.text = email;
_runValidation();
},
);
}).toList(),
),
const SizedBox(height: 16),
// Email Input Field
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Target Email Address',
hintText: 'Enter email to validate...',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.alternate_email),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _emailController.clear(),
),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
// Options Accordion / Card
Card(
elevation: 0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: ExpansionTile(
title: const Text(
'Validation Options & Policies',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
subtitle: Text('Mode: ${_mode.name} | Policy: ${_policy.name}',
overflow: TextOverflow.ellipsis),
initiallyExpanded: false,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
DropdownButtonFormField<EmailValidationMode>(
initialValue: _mode,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Validation Mode',
border: OutlineInputBorder(),
),
items: EmailValidationMode.values
.map((m) => DropdownMenuItem(
value: m,
child: Text(
'Mode: ${m.name.toUpperCase()}',
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (v) {
if (v != null) setState(() => _mode = v);
},
),
const SizedBox(height: 12),
DropdownButtonFormField<DomainPolicy>(
initialValue: _policy,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Domain Filtering Policy',
border: OutlineInputBorder(),
),
items: DomainPolicy.values
.map((p) => DropdownMenuItem(
value: p,
child: Text(
'Policy: ${p.name}',
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (v) {
if (v != null) setState(() => _policy = v);
},
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text('Check Disposable Domains'),
subtitle: const Text('Detect temporary mail services'),
value: _checkDisposable,
onChanged: (v) => setState(() => _checkDisposable = v),
),
SwitchListTile(
title: const Text('Check Fake / Test Domains'),
subtitle: const Text('Detect example.com, localhost, etc.'),
value: _checkFakeDomains,
onChanged: (v) => setState(() => _checkFakeDomains = v),
),
SwitchListTile(
title: const Text('DNS Lookup (DoH)'),
subtitle: const Text('Verify domain exists in DNS'),
value: _checkDns,
onChanged: (v) => setState(() => _checkDns = v),
),
SwitchListTile(
title: const Text('MX Record Lookup (DoH)'),
subtitle: const Text('Verify active mail servers exist'),
value: _checkMx,
onChanged: (v) => setState(() => _checkMx = v),
),
SwitchListTile(
title: const Text('Apply Provider Normalization'),
subtitle: const Text('Strip Gmail dots/tags for canonical ID'),
value: _applyProviderNormalization,
onChanged: (v) => setState(() => _applyProviderNormalization = v),
),
const SizedBox(height: 8),
TextField(
controller: _allowedDomainsController,
decoration: const InputDecoration(
labelText: 'Allowed Custom Domains (comma separated)',
hintText: 'acme.com, school.edu',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _blockedDomainsController,
decoration: const InputDecoration(
labelText: 'Blocked Domains (comma separated)',
hintText: 'competitor.com, badactor.org',
border: OutlineInputBorder(),
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Validate Button
FilledButton.icon(
onPressed: _isLoading ? null : _runValidation,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
icon: _isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.verified_user),
label: Text(
_isLoading ? 'Analyzing Email...' : 'Validate Email Address',
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 24),
// Result Card
if (_result != null) _buildResultCard(_result!),
],
);
}
Widget _buildResultCard(EmailValidationResult res) {
final isValid = res.isValid;
final color = isValid ? Colors.green.shade700 : Colors.red.shade700;
final bgColor = isValid ? Colors.green.shade50 : Colors.red.shade50;
return Card(
elevation: 2,
color: bgColor,
shape: RoundedRectangleBorder(
side: BorderSide(color: color, width: 1.5),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status Header & Provider Badge
Row(
children: [
Icon(isValid ? Icons.check_circle : Icons.cancel, color: color, size: 32),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isValid ? 'Email Accepted' : 'Email Rejected',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
res.reason.name,
style: TextStyle(color: Colors.grey.shade800, fontSize: 13),
overflow: TextOverflow.ellipsis,
),
],
),
),
_buildProviderBadge(res.providerType),
],
),
const Divider(height: 28),
// Message & Normalized Email
_infoTile('Message', res.message, isBold: true),
_infoTile('Normalized', res.normalizedEmail ?? 'N/A'),
if (res.domain != null) _infoTile('Domain', res.domain!),
if (res.validationDuration != null)
_infoTile('Duration', '${res.validationDuration!.inMilliseconds} ms'),
const SizedBox(height: 12),
const Text(
'Detailed Checks Breakdown:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
const SizedBox(height: 8),
// Detailed Flag Grid
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_statusChip('Syntax', res.syntaxValid),
_statusChip('Domain Format', res.domainValid),
_statusChip('Disposable', !res.disposable, trueLabel: 'Not Disposable', falseLabel: 'Disposable'),
_statusChip('Fake/Test', !res.fake, trueLabel: 'Not Fake', falseLabel: 'Fake/Test'),
_statusChip('Custom Policy', res.customDomainAllowed, trueLabel: 'Allowed', falseLabel: 'Not Allowed'),
_statusChip('Blocklist', !res.blocked, trueLabel: 'Unblocked', falseLabel: 'Blocked'),
if (res.dnsValid != null) _statusChip('DNS Lookup', res.dnsValid!),
if (res.mxValid != null) _statusChip('MX Lookup', res.mxValid!),
],
),
],
),
),
);
}
Widget _buildProviderBadge(EmailProviderType provider) {
Color chipColor;
IconData icon;
switch (provider) {
case EmailProviderType.public:
chipColor = Colors.blue.shade700;
icon = Icons.public;
break;
case EmailProviderType.privacy:
chipColor = Colors.purple.shade700;
icon = Icons.security;
break;
case EmailProviderType.disposable:
chipColor = Colors.orange.shade800;
icon = Icons.delete_outline;
break;
case EmailProviderType.business:
chipColor = Colors.teal.shade700;
icon = Icons.business;
break;
case EmailProviderType.unknown:
chipColor = Colors.grey.shade700;
icon = Icons.help_outline;
break;
}
return Chip(
avatar: Icon(icon, size: 16, color: Colors.white),
label: Text(
provider.name.toUpperCase(),
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 11),
),
backgroundColor: chipColor,
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
);
}
Widget _statusChip(
String label,
bool passed, {
String? trueLabel,
String? falseLabel,
}) {
final text = passed ? (trueLabel ?? 'Passed') : (falseLabel ?? 'Failed');
final color = passed ? Colors.green.shade700 : Colors.red.shade700;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(passed ? Icons.check : Icons.close, size: 14, color: color),
const SizedBox(width: 4),
Flexible(
child: Text(
'$label: $text',
style: TextStyle(color: color, fontWeight: FontWeight.w600, fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _infoTile(String label, String value, {bool isBold = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 90,
child: Text(label, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontSize: 13,
color: Colors.black87,
),
),
),
],
),
);
}
}
// ============================================================================
// TAB 2: INSTANT EMAIL CLASSIFIER & NORMALIZER
// ============================================================================
class EmailClassifierTab extends StatefulWidget {
const EmailClassifierTab({super.key});
@override
State<EmailClassifierTab> createState() => _EmailClassifierTabState();
}
class _EmailClassifierTabState extends State<EmailClassifierTab> {
final _controller = TextEditingController(text: 'John.Doe+tag@Gmail.COM');
@override
Widget build(BuildContext context) {
final input = _controller.text;
final normalizedStandard = EmailIntegrity.normalize(input) ?? 'Invalid';
final normalizedGmail = EmailIntegrity.normalize(input, applyProviderNormalization: true) ?? 'Invalid';
final providerType = EmailIntegrity.classifyProvider(input);
final isDisposable = EmailIntegrity.isDisposableEmail(input);
final isStandardPublic = EmailIntegrity.isStandardPublicDomain(
input.contains('@') ? input.split('@').last : input,
);
return ListView(
padding: const EdgeInsets.all(16.0),
children: [
const Text(
'Instant Email Normalizer & Classifier',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
'Classify provider types and test safe normalization rules in real-time.',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Email Address or Domain',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 20),
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_classifierRow('Provider Type', providerType.name.toUpperCase(), Icons.category),
const Divider(),
_classifierRow('Standard Normalization', normalizedStandard, Icons.cleaning_services),
const Divider(),
_classifierRow('Provider Normalization (Gmail)', normalizedGmail, Icons.alternate_email),
const Divider(),
_classifierRow('Is Disposable?', isDisposable ? 'Yes (Temporary)' : 'No', Icons.delete_outline),
const Divider(),
_classifierRow('Is Standard Public Provider?', isStandardPublic ? 'Yes' : 'No', Icons.public),
],
),
),
),
],
);
}
Widget _classifierRow(String label, String value, IconData icon) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(icon, size: 20, color: Colors.deepPurple),
const SizedBox(width: 12),
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
textAlign: TextAlign.end,
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.deepPurple),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
// ============================================================================
// TAB 3: FLUTTER FORM INTEGRATION DEMO
// ============================================================================
class FormValidationTab extends StatefulWidget {
const FormValidationTab({super.key});
@override
State<FormValidationTab> createState() => _FormValidationTabState();
}
class _FormValidationTabState extends State<FormValidationTab> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _allowEmpty = false;
final String _requiredMessage = 'Email address is required.';
final String _invalidMessage = 'Please enter a valid email address.';
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16.0),
children: [
const Text(
'Flutter TextFormField Validator Demo',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
'Demonstrates synchronous, offline-safe FormFieldValidator integration.',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email Address',
hintText: 'Enter your email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email_outlined),
),
keyboardType: TextInputType.emailAddress,
validator: EmailIntegrity.formValidator(
allowEmpty: _allowEmpty,
requiredMessage: _requiredMessage,
invalidMessage: _invalidMessage,
),
),
const SizedBox(height: 16),
Card(
elevation: 0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
SwitchListTile(
title: const Text('Allow Empty Input'),
subtitle: const Text('When enabled, empty fields pass form validation'),
value: _allowEmpty,
onChanged: (v) => setState(() => _allowEmpty = v),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form Validation Passed Successfully!'),
backgroundColor: Colors.green,
),
);
}
},
icon: const Icon(Icons.check_circle_outline),
label: const Text('Submit Form'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
],
),
),
],
);
}
}