Email sender identity normalization and trust/reputation scoring
| Tier | domain |
| Role | unclassified (baselined) |
| Path | crates/domain/sender-reputation |
| Edition | 2021 |
| Targets | domain_sender_reputation |
| Public items | 21 across 5 modules |
| Tests | 27 |
What it is for
# domain-sender-reputation
Email sender identity normalization and trust/bulk reputation scoring — used to decide send-safety (is this a sender worth replying to / a bounce-back safe recipient / a machine sender not worth alerting on).
Sprint 3.45 crate 9 (tools-mail-manager harvest). Lifted from rust-gmail-manager/src/sender/{normalize,scoring,model}.rs — see CHANGELOG.md for the full Gate 4.5 provenance and what was deliberately not lifted.
Two independent concerns
normalize— turn a rawFrom:header into anormalize::CanonicalSender:
lowercased/trimmed address, display name, domain, root domain, and noreply/machine-sender detection. Pure, no I/O.
scoring— turn interaction counts (inbox/spam/trash/starred) into a
0-100 trust score (Bayesian-smoothed) and a 0-100 bulk score, and classify the pair into a scoring::ReputationClass. Pure, no I/O.
models::SenderProfile/models::SenderDomain assemble the two into a persistable record; repository::SenderReputationRepository is the trait a consumer implements against its own store (no concrete backend ships here — see that module's doc comment for why).
Composition, not duplication (Gate 0.5)
normalize::CanonicalSender::to_contact builds a domain_contact::Contact by calling Contact::email rather than inventing a parallel address type — this crate owns parsing raw sender headers into a canonical, scoreable identity, and hands off to domain-contact's existing shape once that identity is linked to a known party.
Example
use domain_sender_reputation::{
models::{InteractionCounts, SenderProfile},
scoring::SenderConfig,
};
use uuid::Uuid;
let config = SenderConfig::default();
let profile = SenderProfile::from_raw_sender(
Uuid::new_v4(),
"Newsletter <news@example.com>",
InteractionCounts {
message_count: 40,
inbox_count: 40,
spam_count: 0,
trash_count: 0,
starred_count: 0,
},
None,
None,
&config,
)
.expect("valid sender header");
assert!(profile.is_machine_sender);
Capabilities
SenderReputationError
Sender-reputation errors.
| Item |
|---|
pub enum SenderReputationError |
InteractionCounts
Sender and sender-domain reputation records.
| Item |
|---|
pub struct InteractionCounts |
SenderDomain
Sender and sender-domain reputation records.
| Item |
|---|
pub struct SenderDomain |
SenderDomain :: fn from_aggregate(tenant_id : Uuid, domain : String, root_domain : String, sender_count : i32, counts : InteractionCounts, first_seen : Option <DateTime <Utc>>, last_seen : Option <DateTime <Utc>>, config : & SenderConfig,) -> Self |
SenderProfile
Sender and sender-domain reputation records.
| Item |
|---|
pub struct SenderProfile |
SenderProfile :: fn from_raw_sender(tenant_id : Uuid, raw_sender : & str, counts : InteractionCounts, first_seen : Option <DateTime <Utc>>, last_seen : Option <DateTime <Utc>>, config : & SenderConfig,) -> Option <Self> |
SenderProfile :: fn from_canonical(tenant_id : Uuid, canonical : & CanonicalSender, counts : InteractionCounts, first_seen : Option <DateTime <Utc>>, last_seen : Option <DateTime <Utc>>, config : & SenderConfig,) -> Self |
CanonicalSender
Sender identity normalization: canonicalize a raw From:-style header
| Item |
|---|
pub struct CanonicalSender |
CanonicalSender :: fn to_contact(& self, tenant_id : Uuid, party_type : PartyType, party_id : Uuid) -> Contact |
fn canonicalize_email(raw : & str) -> Option <CanonicalSender> |
SenderReputationRepository
Sender-reputation persistence trait.
| Item |
|---|
pub trait SenderReputationRepository |
SenderReputationResult
Sender-reputation persistence trait.
| Item |
|---|
pub type SenderReputationResult<T>: Result <T, SenderReputationError> |
SenderSort
Sender-reputation persistence trait.
| Item |
|---|
pub enum SenderSort |
scoring (other)
Trust/bulk reputation scoring: Bayesian-smoothed trust score, a
| Item |
|---|
fn compute_bulk_score(message_count : i32, is_noreply : bool, is_machine_sender : bool, starred_count : i32, inbox_count : i32,) -> f32 |
ReputationClass
Trust/bulk reputation scoring: Bayesian-smoothed trust score, a
| Item |
|---|
pub enum ReputationClass |
ReputationClass :: fn as_str(& self) -> & 'static str |
ReputationClass :: fn fmt(& self, f : & mut std::fmt::Formatter <'_>) -> std::fmt::Result |
fn classify_reputation(trust_score : f32, bulk_score : f32) -> ReputationClass |
SenderConfig
Trust/bulk reputation scoring: Bayesian-smoothed trust score, a
| Item |
|---|
pub struct SenderConfig |
SenderConfig :: fn default() -> Self |
fn compute_trust_score(inbox_count : i32, starred_count : i32, spam_count : i32, trash_count : i32, config : & SenderConfig,) -> f32 |
How to use it
From this crate's own rustdoc:
use domain_sender_reputation::{
models::{InteractionCounts, SenderProfile},
scoring::SenderConfig,
};
use uuid::Uuid;
let config = SenderConfig::default();
let profile = SenderProfile::from_raw_sender(
Uuid::new_v4(),
"Newsletter <news@example.com>",
InteractionCounts {
message_count: 40,
inbox_count: 40,
spam_count: 0,
trash_count: 0,
starred_count: 0,
},
None,
None,
&config,
)
.expect("valid sender header");
assert!(profile.is_machine_sender);
Module structure
domain_sender_reputation
errorsmodelsnormalizerepositoryscoring
flowchart TD n_domain_sender_reputation["domain_sender_reputation"] n_domain_sender_reputation --> n_errors["errors"] n_domain_sender_reputation --> n_models["models"] n_domain_sender_reputation --> n_normalize["normalize"] n_domain_sender_reputation --> n_repository["repository"] n_domain_sender_reputation --> n_scoring["scoring"]
Public surface
`errors`
| Item | What it is |
|---|---|
pub enum SenderReputationError | Errors that can occur in the sender-reputation domain. |
`models`
| Item | What it is |
|---|---|
pub struct InteractionCounts | Interaction counts a reputation score is computed from. |
pub struct SenderProfile | A single sender's reputation record. |
SenderProfile :: fn from_raw_sender(tenant_id : Uuid, raw_sender : & str, counts : InteractionCounts, first_seen : Option <DateTime <Utc>>, last_seen : Option <DateTime <Utc>>, config : & SenderConfig,) -> Option <Self> | Build a SenderProfile by canonicalizing raw_sender and scoring it against counts, using config for the scoring thresholds/priors |
SenderProfile :: fn from_canonical(tenant_id : Uuid, canonical : & CanonicalSender, counts : InteractionCounts, first_seen : Option <DateTime <Utc>>, last_seen : Option <DateTime <Utc>>, config : & SenderConfig,) -> Self | Build a SenderProfile from an already-canonicalized sender. |
pub struct SenderDomain | A domain-level reputation record, aggregated across all senders at that domain. |
SenderDomain :: fn from_aggregate(tenant_id : Uuid, domain : String, root_domain : String, sender_count : i32, counts : InteractionCounts, first_seen : Option <DateTime <Utc>>, last_seen : Option <DateTime <Utc>>, config : & SenderConfig,) -> Self | Build a SenderDomain record from aggregated counts |
`normalize`
| Item | What it is |
|---|---|
pub struct CanonicalSender | A sender identity that has been canonicalized from a raw header value |
CanonicalSender :: fn to_contact(& self, tenant_id : Uuid, party_type : PartyType, party_id : Uuid) -> Contact | Build the domain_contact::Contact this sender would be, once linked to a known party |
fn canonicalize_email(raw : & str) -> Option <CanonicalSender> | Canonicalize a sender email address |
`repository`
| Item | What it is |
|---|---|
pub type SenderReputationResult<T>: Result <T, SenderReputationError> | Result type for sender-reputation persistence operations. |
pub enum SenderSort | Sort order for listing sender/domain reputation records. |
pub trait SenderReputationRepository | Repository trait for sender/domain reputation persistence. |
`scoring`
| Item | What it is |
|---|---|
pub struct SenderConfig | Configuration for sender reputation scoring. |
SenderConfig :: fn default() -> Self | — |
pub enum ReputationClass | Reputation classification derived from a sender's trust and bulk scores. |
ReputationClass :: fn as_str(& self) -> & 'static str | The mm-compatible string form ("trusted", "likely_trusted", ...). |
ReputationClass :: fn fmt(& self, f : & mut std::fmt::Formatter <'_>) -> std::fmt::Result | — |
fn compute_trust_score(inbox_count : i32, starred_count : i32, spam_count : i32, trash_count : i32, config : & SenderConfig,) -> f32 | Compute a 0-100 trust score using Bayesian smoothing over inbox/starred (positive) and spam/trash (negative) interaction counts |
fn compute_bulk_score(message_count : i32, is_noreply : bool, is_machine_sender : bool, starred_count : i32, inbox_count : i32,) -> f32 | Compute a 0-100 bulk score from message volume and machine-sender signals |
fn classify_reputation(trust_score : f32, bulk_score : f32) -> ReputationClass | Classify a sender into a reputation class from its trust and bulk scores. |
Re-exports. Exported here, defined elsewhere.
| Export | Defined in |
|---|---|
SenderReputationError | errors::SenderReputationError |
{InteractionCounts,SenderDomain,SenderProfile} | models::{InteractionCounts,SenderDomain,SenderProfile} |
{SenderReputationRepository,SenderReputationResult,SenderSort} | repository::{SenderReputationRepository,SenderReputationResult,SenderSort} |
{canonicalize_email,CanonicalSender} | normalize::{canonicalize_email,CanonicalSender} |
{classify_reputation,compute_bulk_score,compute_trust_score,ReputationClass,SenderConfig,} | scoring::{classify_reputation,compute_bulk_score,compute_trust_score,ReputationClass,SenderConfig,} |
Boundary
Reaches into foundation.
Shares tier domain with 41 other crates: domain-agreements, domain-ai-report, domain-billing, domain-catalog, domain-classify, domain-comments, domain-competitive-intel, domain-contact, … (41 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | domain |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/domain/sender-reputation |
| Vocabulary in force (lexicon) | current |
Tier flow. Which tiers this crate's own edges cross.
flowchart LR n_domain["domain"] --> n_foundation["foundation"]
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `domain-contact` | domain | no | always |
| `foundation-mail-message` | foundation | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
async-trait | ^0.1 | — | no | always |
chrono | ^0.4 | serde | no | always |
serde | ^1 | derive | no | always |
serde_json | ^1 | — | no | always |
thiserror | ^2 | — | no | always |
uuid | ^1 | v4, v7, serde, js | no | always |
Development, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
tokio | ^1 | full | no | always |
tokio-test | ^0.4 | — | no | always |
Build. None.
Depended on by. Nothing in this workspace.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR SELF["domain-sender-reputation"] SELF -->|runtime| n_domain_contact["domain-contact"] SELF -->|runtime| n_foundation_mail_message["foundation-mail-message"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| lib | domain_sender_reputation | `src/lib.rs` |
Error model
| Error type | Named by |
|---|---|
SenderReputationError | SenderReputationResult |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | none detected |
| async runtime | none detected |
| database access | none detected |
| network I/O | none detected |
| unsafe code | none detected |
| environment variables | none detected |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
No environment variable is read with a literal name anywhere in this crate. A variable whose key is computed at run time cannot be listed here, and is not claimed to be absent.
Related capabilities
No workspace crate depends on this one.
Verification
| Kind | Count |
|---|---|
| Unit tests | 27 |
| Integration tests | 0 |
| Examples | 0 |
| Doctests | 1 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
errors | 1 | 0 | 0 |
models | 3 | 0 | 0 |
normalize | 2 | 0 | 0 |
repository | 3 | 0 | 0 |
scoring | 5 | 0 | 0 |
What the tests establish, by name:
test_domain_aggregate_ignores_sender_level_machine_flags—src/models.rstest_from_raw_sender_noreply_is_bulk_leaning—src/models.rstest_from_raw_sender_rejects_unparseable_header—src/models.rstest_from_raw_sender_scores_and_classifies—src/models.rstest_canonicalize_lowercases_email—src/normalize.rstest_canonicalize_preserves_plus_alias—src/normalize.rstest_canonicalize_trims_whitespace—src/normalize.rstest_extract_domain—src/normalize.rstest_extract_from_angle_brackets—src/normalize.rstest_human_sender_not_machine—src/normalize.rstest_invalid_email_returns_none—src/normalize.rstest_noreply_detected—src/normalize.rstest_quoted_sender—src/normalize.rstest_to_contact_uses_canonical_email_as_value—src/normalize.rstest_count_senders_scoped_by_tenant—src/repository.rstest_get_sender_missing_returns_none—src/repository.rstest_upsert_then_get_round_trips—src/repository.rstest_bulk_score_bounded—src/scoring.rstest_classify_bulk—src/scoring.rstest_classify_mixed—src/scoring.rstest_classify_trusted—src/scoring.rstest_human_low_volume_low_bulk—src/scoring.rstest_noreply_high_bulk—src/scoring.rstest_reputation_class_as_str_matches_mm—src/scoring.rstest_spammy_sender_low_trust—src/scoring.rstest_trust_score_bounded—src/scoring.rstest_trusted_sender_high_trust—src/scoring.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 19 | 21 |
Public modules with a //! block | 5 | 5 |
pie showData
title Public items with rustdoc
"Documented" : 19
"No rustdoc detected" : 2
Metrics
| Metric | Value |
|---|---|
| Rust source files | 6 |
| Source lines | 1067 |
| Code lines | 725 |
| Public API items | 21 |
| Public modules | 5 |
| Tests | 27 |
| Examples | 0 |
| Cargo features | 0 |
| Direct runtime dependencies | 8 |
| Workspace reverse dependencies | 0 |
pie showData
title Public API by kind
"enum" : 3
"function" : 4
"method" : 7
"struct" : 5
"trait" : 1
"type alias" : 1
pie showData
title Rust source composition
"Code" : 725
"Blank or comment" : 342
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.