Files
Juan Denis 6296887bad Remove internal plan citations from app code
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
2026-10-10 15:50:45 -04:00

49 lines
1.8 KiB
Python

"""Email bounce / complaint suppression state (roadmap #24).
One row per email address that a provider reported a bounce or complaint for.
A hard-bouncing address auto-mutes after ``MUTE_THRESHOLD`` consecutive hard
bounces (a spam complaint mutes immediately); a muted address is skipped by the
bus's email channel until an admin/user unmutes it. Fed by the signed inbound
webhook (``POST /api/v1/notifications/inbound/email``).
"""
from datetime import datetime
from app import db
from app.models.mixins import TimestampMixin
class EmailBounceState(TimestampMixin, db.Model):
__tablename__ = 'email_bounce_state'
KIND_BOUNCE = 'bounce'
KIND_COMPLAINT = 'complaint'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
consecutive_bounces = db.Column(db.Integer, default=0, nullable=False)
total_events = db.Column(db.Integer, default=0, nullable=False)
muted = db.Column(db.Boolean, default=False, nullable=False, index=True)
muted_at = db.Column(db.DateTime, nullable=True)
last_kind = db.Column(db.String(20), nullable=True)
last_reason = db.Column(db.String(500), nullable=True)
last_event_at = db.Column(db.DateTime, nullable=True)
def to_dict(self):
return {
'email': self.email,
'muted': bool(self.muted),
'consecutive_bounces': self.consecutive_bounces or 0,
'total_events': self.total_events or 0,
'last_kind': self.last_kind,
'last_reason': self.last_reason,
'last_event_at': self.last_event_at.isoformat() if self.last_event_at else None,
'muted_at': self.muted_at.isoformat() if self.muted_at else None,
}
def __repr__(self):
return f'<EmailBounceState {self.email} muted={self.muted} n={self.consecutive_bounces}>'