Production-grade automated email dispatch for arXiv endorsement outreach
Project description
arXiv Endorsement Dispatch Engine
Automated, production-grade email dispatch system for requesting arXiv endorsements. Handles the full pipeline — parsing endorser lists, rendering personalized emails, routing through multiple SMTP accounts with health-aware load balancing, retries, bounce handling, and deliverability analytics.
Author: Nixon-H
Disclaimer: Use responsibly. Respect recipient privacy, follow anti-spam regulations (CAN-SPAM, GDPR), and only contact researchers who have indicated openness to endorsement requests.
Features
Core Dispatch Engine
- Multi-account routing — health-aware adaptive selection: picks the healthiest account by weighted success rate + average latency
- Per-provider rate limiting — gmail 20/h 200/d, outlook 15/h 150/d (configurable per account), enforced in-process + persisted in DB across restarts
- SMTP connection pooling —
SmtpConnectionPoolreuses authenticated connections, lazy reconnects on failure - SMTP capability detection — after EHLO+STARTTLS+EHLO, inspects
esmtp_features: STARTTLS, AUTH (with supported mechanisms listed: LOGIN/PLAIN/XOAUTH2...), SIZE, PIPELINING, 8BITMIME, SMTPUTF8, DSN. Exposed viaget_account_capabilities()/all_capabilities() - Provider fingerprinting — SMTP banner matched against 12 provider patterns (Google, M365/Exchange, Postfix, Exim, Sendmail, Zimbra, etc.) with domain-based fallback
- Error classification — 8-class taxonomy (
SmtpErrorClass): authentication, permanent, temporary, timeout, rate-limited, DNS, hard/soft bounce, TLS, unknown — each with a recovery strategy - Randomized exponential backoff with jitter (
2^n * rand(0.8, 1.5)) between retries - Bounce processing — 30+ SMTP response codes (421→554) mapped to actions + bounce types, regex text-pattern fallback, bounce DB table, hard-bounced recipients auto-skipped on future runs, recovery advice per bounce type
- DNS validation — MX lookup (dnspython w/ dig fallback), A-record fallback, disposable-email domain blacklist
- Parallel DNS validation —
validate_emails_parallel()via ThreadPoolExecutor - Email fingerprinting — SHA256 body hash stored per send to prevent duplicate sends
- Correlation IDs —
uuid4().hex[:12]generated per send, threaded through the full pipeline (SMTP → DB → structured logs) so one send is traceable end-to-end
Data & Parsing
- Multi-format input: TXT / CSV / JSON / YAML / XLSX with magic-byte + extension auto-detection (TXT supports block format or
Name | email | paperpipe-separated lines) - Unicode normalization (NFKC) on all name/paper fields
- In-file duplicate detection with per-category stats:
- Duplicate email
- Duplicate (name + paper)
- Duplicate exact record hash
- Reported as
Duplicates skipped — Email: N, Name/Paper: N, Exact: N
- PDF header validation (
%PDF-magic) for your manuscript - File integrity + checksum verification (SHA256/MD5)
- Contact history per recipient (first/last contact, total sends, bounces)
Template & Content Engine
- Multi-template rotation with HTML + plain-text pair support
- Subject-line rotation (anti-spam-signature diversity)
- Signature profiles with variants
- Auto plain-text generation from HTML (
strip_html) - Template caching with mtime-based auto-reload (
get_cache_stats()) - Template linting — greeting presence, lines >120 chars, multiple spaces, empty paragraphs
- HTML sanity checks — tag balance verification for 12 HTML tags
- Render validation —
get_required_vars()+validate_context()ensures all{{ var }}placeholders resolve - Email quality scoring — 0–100 composite score with A–F grade: missing subject/body, 30+ spam trigger words, broken links, attachment validation, lint + HTML issues — shown per email in dry-run and live sends
- Anti-spam hygiene:
X-Mailer,User-Agent,Reply-To, custom headers (no bulk-mailList-Unsubscribe— deliberate, keeps personal outreach out of bulk classification) - Archive sent emails — every successful send saved as
sent/YYYY-MM-DD_Name.eml(full MIME structure)
Safety & Reliability
- File locking (
fcntl.flock) — prevents concurrent runs on the same state - Atomic SQLite transactions — crash recovery without corruption,
VACUUMsupport - Versioned schema migrations —
schema_version+migration_historytables (currently schema v4: latency_details, body_fingerprint + smtp_conversation, correlation_id migrations) - Auto-backup of progress state (
data/backups/progress_*.json) - Secret redaction in all logs — 7 regex patterns + env-var auto-detection,
***REDACTED*** - Immutable config — frozen dataclasses post-validation (
config_version: 2, auto-upgrade from v1) - Encrypted credential store — Fernet-encrypted
.credentials.enc(cryptography lib) - Pre-flight checks (
--verify): templates, PDF, config, accounts, DNS --doctordiagnostic (16+ checks): file existence, imports, writability, CLI tools, Python version, SMTP auth, DKIM/SPF/DMARC (SPF record, DKIM selector, DMARC policy via dig), TLS cert info (issuer/subject/expiry), attachment validation, template git-diff detection- Diagnostic bundle —
--doctor=bundle→diagnostics.zip(config, logs, DB, templates, plugins) - Interactive resume prompt before batch sends ("Resume from #N? [Enter=yes, n=restart]")
Observability
- Structured JSON logging (
.jsonl) with severity, recipient, account, status, latency, correlation ID - Per-phase delivery latency — DNS → SMTP connect → EHLO → TLS handshake → AUTH → DATA → TOTAL, stored as JSON in DB
- Prometheus metrics endpoint (
/metrics) — counters, gauges, histograms (sends, failures, latency) - Rich HTML dashboard — SVG pie charts (outcome distribution, duplicate breakdown), send-latency timeline (last 50 sends), account health table with success rates
- Export reports: JSON / CSV / HTML
- Domain reputation report — per-domain success rate + avg latency table in
--stats - Execution stats dashboard (
--stats) with runtime counters
Extensibility
- Plugin architecture — drop-in
plugins/directory, auto-discovered, 6 lifecycle hooks:@hook_before_send/@hook_after_send@hook_before_validate/@hook_after_failure@hook_after_retry/@hook_before_archive
- i18n (en / fr / zh) via JSON locale files,
--localeflag - Notifications: Discord / Slack / Desktop (notify-send / osascript / win10toast), completion summaries
- Interactive setup wizard —
--initguides multi-account config creation - Named SMTP profiles in config (multiple providers per account entry)
- Scheduler generation: cron line + systemd service/timer units
arxiv-mailCLI — pip-installable entry point
Developer Experience
- 100 tests across 9 test files (unit + fuzz), 0.29s runtime
- Fuzz testing — random malformed TXT/CSV/JSON/binary/huge-lines/special-chars thrown at the parser
- GitHub Actions CI — ruff lint, mypy typecheck, test matrix (Python 3.10/3.11/3.12 × Ubuntu/macOS/Windows), coverage upload, bandit + pip-audit + safety security scanning
- Pre-commit hooks — ruff, mypy, bandit, yaml/json fixers
- Makefile —
make test / lint / typecheck / security / doctor / clean / all - Semantic release config with conventional-commit enforcement
- Docs: README, CHANGELOG, CONTRIBUTING (with architecture + sequence diagrams), SECURITY, MIT LICENSE
Installation
git clone https://github.com/Nixon-H/arxiv-automation.git
cd arxiv_automation
pip install -e .
Optional dependencies
pip install openpyxl # XLSX parsing
pip install pyyaml # YAML parsing
pip install dnspython # DNS validation
pip install cryptography # encrypted credential store
pip install prometheus-client # metrics
Quick Start
1. Configure
Edit config.json:
{
"sender_identity": {
"your_name": "Nixon-H",
"your_paper_title": "My Paper Title",
"arxiv_category": "cs.AI"
},
"safety_and_limits": {
"cooldown_hours": 24.0,
"smtp_timeout_seconds": 30.0,
"random_delay_range_seconds": [5, 15],
"max_retries": 3,
"initial_backoff_seconds": 2.0
},
"accounts": [
{
"provider": "gmail",
"email": "you@gmail.com",
"password": "${SMTP_PASSWORD_1}",
"server": "smtp.gmail.com",
"port": 587,
"max_per_hour": 20,
"max_per_day": 200
}
],
"config_version": 2
}
2. Set passwords
Create .env (gitignored):
SMTP_PASSWORD_1=your-gmail-app-password-here
Gmail requires an App Password (2-Step Verification must be enabled):
- https://myaccount.google.com/security → enable 2-Step Verification
- https://myaccount.google.com/apppasswords → create app password
- Put it in
.env(spaces removed)
3. Prepare your materials
If
endorsers.txtis missing, the first run creates a sample file for you to edit — you don't have to start from scratch.
endorsers.txt— one record per endorser (see sample format below)template.txt/template.html— your email templates with{{ var }}placeholdersmy_paper.pdf— your manuscript (validated for PDF header)
4. Preview & test
python run.py --dry-run # Preview next dispatch (renders + writes preview.html)
python run.py --verify # Pre-flight validation checks
python run.py --test you@example.com # Send a test email
5. Go live
python run.py --live # Send one email
python run.py --live --send 5 # Send a batch of 5
python run.py --retry-failed # Retry previously failed sends
CLI Reference
| Command | Description |
|---|---|
--dry-run |
Preview next dispatch, generate preview.html |
--test [EMAIL] |
Send test email to verify configuration |
--live |
Execute live dispatch (use with --send N) |
--send N |
Batch size for --live |
--stats |
Display execution statistics + domain reputation table |
--verify |
Pre-flight validation |
--doctor / --doctor=bundle |
Full diagnostic / diagnostic zip bundle |
--export-report |
Generate JSON/CSV/HTML reports |
--validate-config |
Validate configuration file |
--retry-failed |
Retry failed dispatches |
--reset-progress |
Reset all tracking data |
--scheduler |
Generate cron + systemd scheduler config |
--metrics [PORT] |
Prometheus metrics endpoint (default 9090) |
--init |
Interactive setup wizard |
--locale {en,fr,zh} |
i18n locale |
--no-browser |
Don't auto-open preview.html |
Input Format
endorsers.txt (whitespace or | separated):
Dr. Elena Vasquez | elena.vasquez@example.com | Deep Learning for Safety
Prof. Marcus Chen | marcus.chen@example.com | Deep Learning Theory
CSV / JSON / YAML / XLSX also supported — must contain last_name, email, paper_title fields.
Templates
template.txt (plain text):
Dear Dr./Mr./Ms. {{ last_name }},
I trust this email finds you in good spirits and good health.
My name is {{ your_name }}, and I am an independent researcher. I have recently completed an academic paper titled "{{ your_paper_title }}" and would be honored to have your endorsement for submission to arXiv's {{ arxiv_category }} category.
...
Available variables: {{ last_name }}, {{ email }}, {{ paper_title }}, {{ your_name }}, {{ your_paper_title }}, {{ arxiv_category }}, {{ signature }}
Project Structure
arxiv_automation/
├── run.py # CLI entry point (14 commands)
├── config.json # Config with ${ENV_VAR} interpolation
├── .env # SMTP passwords (gitignored)
├── pyproject.toml # Packaging, CLI entry, coverage, ruff config
├── Makefile # test / lint / typecheck / security / doctor / clean / all
├── .pre-commit-config.yaml # ruff + mypy + bandit hooks
├── .github/workflows/ci.yml # CI: lint, typecheck, test matrix, security
│
├── core/
│ ├── config.py # Config loader + version auto-upgrade
│ ├── config_typed.py # Frozen dataclasses (SMTPAccount, TypedConfig, ...)
│ ├── database.py # SQLite + versioned migrations (schema v4)
│ ├── tracker.py # Progress + AccountHealth + auto-backup
│ ├── validator.py # Email, PDF, checksum, pre-flight
│ ├── logger.py # Structured JSON logs + secret redaction
│ ├── ratelimiter.py # Per-provider rate limiting
│ ├── dns_validator.py # MX/SPF/DKIM/DMARC + parallel validation
│ ├── doctor.py # Diagnostic checks + diagnostics.zip bundle
│ ├── lock.py # fcntl.flock file locking
│ ├── metrics.py # Prometheus endpoint
│ ├── notifications.py # Discord / Slack / Desktop
│ ├── secrets.py # Env var resolution
│ ├── credential_store.py # Fernet-encrypted credentials
│ ├── wizard.py # Interactive setup wizard
│ ├── email_quality.py # Spam triggers, scoring, linting
│ ├── exceptions.py # 8 exception classes
│ └── i18n.py # en/fr/zh locales
│
├── parsing/
│ ├── parser.py # TXT/CSV/JSON/YAML/XLSX + dedup
│ └── bounce.py # SMTP response code map + bounce classification
│
├── engine/
│ ├── smtp.py # Pooling, capability detection, latency phases
│ ├── templates.py # Rotation, caching, validation
│ ├── orchestrator.py # Pipeline runner
│ └── plugins.py # Lifecycle hook system
│
├── exports/
│ └── report.py # JSON/CSV/HTML dashboard
│
├── tests/ # 100 tests (unit + fuzz)
├── locales/ # en.json, fr.json, zh.json
└── plugins/ # drop-in plugin directory
Development
make lint # ruff check
make typecheck # mypy
make test # pytest with coverage
make security # bandit
make doctor # system diagnostic
make all # lint + typecheck + test + security + doctor
Pre-commit hooks are configured; run pre-commit install to enable.
Testing
100 tests across 9 test files — unit tests for parser, validator, templates, plugins, notifications, database, exports, SMTP classification, plus fuzz tests that throw random malformed TXT/CSV/JSON/binary at the parser to guarantee it never crashes.
Security
- Passwords only in
.env(gitignored) or Fernet-encrypted credential store - Secrets redacted from all logs
- Config is immutable post-validation
- Dependency scanning (pip-audit, safety) and static analysis (bandit) in CI
- See SECURITY.md for the vulnerability disclosure policy
License
MIT — see LICENSE.
Roadmap
- Real-provider validation of deliverability (Gmail/Outlook bounce behavior)
- OAuth2 SMTP (XOAUTH2) for Gmail without app passwords
- Plugin API versioning + deprecation framework
- OpenTelemetry tracing with per-email event timelines
- SBOM + reproducible builds in CI
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arxiv_automation-5.1.0.tar.gz.
File metadata
- Download URL: arxiv_automation-5.1.0.tar.gz
- Upload date:
- Size: 70.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4a112a44a67f46f8ea9e1e04aa9a294c1b72fbf9284d0fda7211202194df1e1
|
|
| MD5 |
062c88f98e5032b98513e7b344b1b187
|
|
| BLAKE2b-256 |
49637f670f9cbe19e9e4ac6784a7ea69f91665ea0639a7eb02efbc54b63e0cf4
|
Provenance
The following attestation bundles were made for arxiv_automation-5.1.0.tar.gz:
Publisher:
publish.yml on Nixon-H/arxiv-automation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arxiv_automation-5.1.0.tar.gz -
Subject digest:
a4a112a44a67f46f8ea9e1e04aa9a294c1b72fbf9284d0fda7211202194df1e1 - Sigstore transparency entry: 2305037628
- Sigstore integration time:
-
Permalink:
Nixon-H/arxiv-automation@99aaad8ec6d1dd5513289e9721efe56a0c567b66 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Nixon-H
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99aaad8ec6d1dd5513289e9721efe56a0c567b66 -
Trigger Event:
push
-
Statement type:
File details
Details for the file arxiv_automation-5.1.0-py3-none-any.whl.
File metadata
- Download URL: arxiv_automation-5.1.0-py3-none-any.whl
- Upload date:
- Size: 68.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ceec9ec4749d4d3b76af7311f63533371f1250b775e8a97ccebfa4e22736107a
|
|
| MD5 |
beaf8df43ce9be252db46f704617f4a1
|
|
| BLAKE2b-256 |
b28c505ec3505bc8ef3dbcf86fb8e4c9f7fd5ef230951f81a9d62853034d48cd
|
Provenance
The following attestation bundles were made for arxiv_automation-5.1.0-py3-none-any.whl:
Publisher:
publish.yml on Nixon-H/arxiv-automation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arxiv_automation-5.1.0-py3-none-any.whl -
Subject digest:
ceec9ec4749d4d3b76af7311f63533371f1250b775e8a97ccebfa4e22736107a - Sigstore transparency entry: 2305037789
- Sigstore integration time:
-
Permalink:
Nixon-H/arxiv-automation@99aaad8ec6d1dd5513289e9721efe56a0c567b66 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Nixon-H
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99aaad8ec6d1dd5513289e9721efe56a0c567b66 -
Trigger Event:
push
-
Statement type: