{"id":29867100,"url":"https://github.com/bookshelf-writer/puremail","last_synced_at":"2025-07-30T13:20:07.872Z","repository":{"id":306342256,"uuid":"1025849130","full_name":"Bookshelf-Writer/puremail","owner":"Bookshelf-Writer","description":"A blazing‑fast, zero‑allocation Go package for strict e‑mail parsing, tag trimming and binary serialisation","archived":false,"fork":false,"pushed_at":"2025-07-25T00:50:46.000Z","size":45,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-25T05:15:14.255Z","etag":null,"topics":["email","golang","gomod","normalize","parser","valid","zero-alloc"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Bookshelf-Writer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-24T22:59:49.000Z","updated_at":"2025-07-25T00:50:50.000Z","dependencies_parsed_at":"2025-07-25T05:15:23.043Z","dependency_job_id":"f1baa2f5-3f74-4aa7-8c65-ca6b9482e1ad","html_url":"https://github.com/Bookshelf-Writer/puremail","commit_stats":null,"previous_names":["bookshelf-writer/puremail"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/Bookshelf-Writer/puremail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bookshelf-Writer%2Fpuremail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bookshelf-Writer%2Fpuremail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bookshelf-Writer%2Fpuremail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bookshelf-Writer%2Fpuremail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Bookshelf-Writer","download_url":"https://codeload.github.com/Bookshelf-Writer/puremail/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bookshelf-Writer%2Fpuremail/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267874843,"owners_count":24158767,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-30T02:00:09.044Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["email","golang","gomod","normalize","parser","valid","zero-alloc"],"created_at":"2025-07-30T13:20:02.908Z","updated_at":"2025-07-30T13:20:07.860Z","avatar_url":"https://github.com/Bookshelf-Writer.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Go Report Card](https://goreportcard.com/badge/github.com/Bookshelf-Writer/puremail)](https://goreportcard.com/report/github.com/Bookshelf-Writer/puremail)\n\n![GitHub repo file or directory count](https://img.shields.io/github/directory-file-count/Bookshelf-Writer/puremail?color=orange)\n![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/Bookshelf-Writer/puremail?color=green)\n![GitHub repo size](https://img.shields.io/github/repo-size/Bookshelf-Writer/puremail)\n\n# puremail\n\nA **zero‑allocation**, high‑throughput Go library for *strict* e‑mail parsing, tag trimming, binary\nserialisation and DNS‑MX probing.  \nThe parser normalises case, removes disposable `+` / `=` tags **before** validation, caches its own\nresults and lets you hash or encode an address in a single line.\n\n\u003e **Focus:** production back‑ends that need predictable latency and memory\n\u003e footprint. Exhaustive RFC‑5322 edge‑cases are intentionally ignored.\n\n---\n\n## Features\n\n| ✔                                   | Description                                                                 |\n|-------------------------------------|-----------------------------------------------------------------------------|\n| **Prefix trimming**                 | `bob+promo=gophers@gmail.com` → `bob@gmail.com` (prefixes kept internally). |\n| **RFC‑ish validation**              | Login \u0026 domain checked against a pragmatic subset of the RFC.               |\n| **Parser cache**                    | Same address parsed only once thanks to `singleflight`; toggle via config.  |\n| **MX probing with smart cache**     | `HasMX()` uses a sharded, TTL‑aware cache with concurrency limits.          |\n| **CRC‑protected bytes**             | `Bytes()` / `Decode()` round‑trip with CRC‑32 guard.                        |\n| **BLAKE2b‑160 hashes**              | `Hash()` (login+domain) \u0026 `HashFull()` (including prefixes).                |\n| **100 % allocation‑free fast path** | All hot methods avoid heap use.                                             |\n| **Fuzz‑tested \u0026 benchmarked**       | \u003e500 k/s parse on a single core (see `go test -bench .`).                   |\n\n---\n\n## Installation\n\n```bash\ngo get github.com/Bookshelf-Writer/puremail\n```\n\n---\n\n## Quick start\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/Bookshelf-Writer/puremail\"\n)\n\nfunc main() {\n\t// Initialize with default configuration\n\tpuremail.InitDefault()\n\n\taddr, err := puremail.New(\"Alice+dev=go@example.io\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfmt.Println(addr.Mail())        // alice@example.io\n\tfmt.Println(addr.MailFull())    // alice+dev=go@example.io\n\tfmt.Printf(\"%x\\n\", addr.Hash()) // 20‑byte BLAKE2b‑160\n}\n```\n\nThe package can be configured with a custom ConfigObj:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/Bookshelf-Writer/puremail\"\n)\n\nfunc main() {\n\tconfig := puremail.ConfigObj{\n\t\tNoCache: false,\n\t\tMX: puremail.ConfigMxObj{\n\t\t\tTllPos:       12 * time.Hour,\n\t\t\tTllNeg:       30 * time.Minute,\n\t\t\tRefreshAhead: 20 * time.Minute,\n\n\t\t\tTimeoutDns:      500 * time.Millisecond,\n\t\t\tTimeoutDnsBurst: 3 * time.Second,\n\t\t\tTimeoutRefresh:  60 * time.Second,\n\n\t\t\tShardAbs:     8,\n\t\t\tShardMaxSize: 20_000,\n\n\t\t\tConcurrencyLimitLookupMX: 500,\n\t\t},\n\t\tCtx: context.Background(),\n\t}\n\n\tpuremail.Init(config)\n\n\t// Use the package functions...\n}\n```\n\n---\n\n## Configuration (`ConfigObj`)\n\n| Field       | Type              | Purpose / default                                                            |\n|-------------|-------------------|------------------------------------------------------------------------------|\n| **NoCache** | `bool`            | `true` disables the internal *singleflight* cache used by `New` / `NewFast`. |\n| **MX**      | `ConfigMxObj`     | Nested object that tunes the MX resolver cache (see below).                  |\n| **Ctx**     | `context.Context` | Root context for background goroutines. Defaults to `context.Background()`.  |\n\n### `ConfigMxObj`\n\n| Field                      | Default  | What it does                                                       |\n|----------------------------|----------|--------------------------------------------------------------------|\n| `TllPos`                   | `6h`     | TTL for *positive* MX answers.                                     |\n| `TllNeg`                   | `15m`    | TTL for *negative* answers (NXDOMAIN / no records).                |\n| `RefreshAhead`             | `10m`    | Time **before** TTL when an entry may be refreshed asynchronously. |\n| `TimeoutDns`               | `400ms`  | Hard limit for a single DNS lookup.                                |\n| `TimeoutDnsBurst`          | `2s`     | Upper bound when many lookups queue at once.                       |\n| `TimeoutRefresh`           | `90s`    | How often the cleaner scans \u0026 evicts expired items.                |\n| `ShardAbs`                 | `4`      | log₂ of cache shards ⇒ `2⁴ = 16` shards (1 .. 31).                 |\n| `ShardMaxSize`             | `10 000` | Max entries per shard (oldest drop first).                         |\n| `ConcurrencyLimitLookupMX` | `250`    | Global semaphore guarding parallel DNS queries.                    |\n\n\u003e Call `puremail.Init(\u0026cfg)` once at program start.\n\u003e Calling nothing is identical to `puremail.InitDefault()`.\n\n---\n\n## Constructors\n\n| Function            | Behaviour                                               |\n|---------------------|---------------------------------------------------------|\n| `New(s string)`     | Validates and **trims prefixes** (`+`, `=`).            |\n| `NewFast(s string)` | Same validation, but prefixes are not treated (faster). |\n\n---\n\n## API reference\n\n### `EmailObj` methods\n\n| Method       | Returns            | Comment                                                    |\n|--------------|--------------------|------------------------------------------------------------|\n| `Login()`    | `string`           | Local part without prefixes.                               |\n| `Domain()`   | `string`           | Domain in lower‑case.                                      |\n| `Prefixes()` | `[]EmailPrefixObj` | Slice of preserved prefixes.                               |\n| `Mail()`     | `string`           | Canonical `\u003clogin\u003e@\u003cdomain\u003e`.                              |\n| `MailFull()` | `string`           | Original address with prefixes.                            |\n| `String()`   | `string`           | Debug representation.                                      |\n| `Bytes()`    | `[]byte`           | Binary payload + CRC‑32.                                   |\n| `Hash()`     | `[20]byte`         | BLAKE2b‑160 of login+domain.                               |\n| `HashFull()` | `[20]byte`         | Same, but includes prefixes.                               |\n| `HasMX()`    | `error`            | `nil` if at least one MX exists. Cached, concurrency‑safe. |\n\n### `EmailPrefixObj`\n\n| Method     | Purpose                         |\n|------------|---------------------------------|\n| `String()` | Original text (`\"dev\"`).        |\n| `Prefix()` | Delimiter char (`'+'` / `'='`). |\n\n### Stand‑alone helpers\n\n| Function         | Use case                                 |\n|------------------|------------------------------------------|\n| `Decode([]byte)` | Recreate `EmailObj` from `Bytes()` blob. |\n\n---\n\n## Usage examples\n\n```go\naddr, _ := puremail.New(\"bob+promo=gophers@gmail.com\")\n\n// 1. Basic fields\nfmt.Println(addr.Login())        // bob\nfmt.Println(addr.Domain()) // gmail.com\nfmt.Println(addr.Mail()) // bob@gmail.com\nfmt.Println(addr.MailFull()) // bob+promo=gophers@gmail.com\nfmt.Println(addr.String()) // [ 'bob@gmail.com', ['+promo', '=gophers'] ]\n\n// 2. Prefix enumeration\nfor _, p := range addr.Prefixes() {\nfmt.Printf(\"tag %c = %s\\n\", p.Prefix(), p.String())\n}\n\n// 3. Hashes\nfmt.Printf(\"stable hash  : %x\\n\", addr.Hash())\nfmt.Printf(\"hash w/tags  : %x\\n\", addr.HashFull())\n\n// 4. Binary round‑trip\nblob := addr.Bytes()\nback, _ := puremail.Decode(blob)\nfmt.Println(back.Mail()) // bob@gmail.com\n\n// 5. MX check (cached)\nif err := addr.HasMX(); err != nil {\nlog.Printf(\"domain has no MX: %v\", err)\n}\n\n// 6. NewFast: keep prefixes\nfast, _ := puremail.NewFast(\"bob+promo=gophers@gmail.com\")\nfmt.Println(fast.MailFull()) // unchanged\n```\n\n---\n\n## Encoding / decoding in detail\n\n```go\ne, _ := puremail.New(\"alice+dev=go@example.io\")\npayload := e.Bytes() // safe to store in Redis or pass over the wire\nagain, err := puremail.Decode(payload)\nif err != nil { panic(err) }\n```\n\nThe format is:\n\n```\n\u003clen(login)\u003e\u003clogin\u003e\u003clen(domain)\u003e\u003cdomain\u003e[ \u003ctag\u003e\u003clen(txt)\u003e\u003ctxt\u003e ... ]\u003ccrc‑32LE\u003e\n```\n\nAny corruption (or truncated payload) is caught by the CRC check.\n\n---\n\n## MX cache life‑cycle\n\n```\n┌─parse.HasMX()──────────────────┐\n│ shard lookup (CRC‑32 hash)     │  ← constant‑time\n│ ├─ fresh? → return             │\n│ └─ group.Do(domain, dnsQuery)  │  ← singleflight + semaphore\n└────────────────────────────────┘\n```\n\n* Positive TTL (`TllPos`) and negative TTL (`TllNeg`) are fully configurable.\n* A background goroutine prunes expired entries every `TimeoutRefresh`.\n* Cache size is bounded per shard; oldest keys are dropped.\n\n---\n\n## Limitations\n\n* ASCII input only; supply punycode yourself (`пример.укр` → `xn--e1afmkfd.xn--j1amh`).\n* No quoted‑local‑part, comments or IP‑literals.\n* Max total length **254 bytes**.\n* `HasMX()` issues network DNS lookups (honours context cancellation).\n\n---\n\n---\n\n### Mirrors\n\n- https://git.bookshelf-writer.fun/Bookshelf-Writer/puremail","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbookshelf-writer%2Fpuremail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbookshelf-writer%2Fpuremail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbookshelf-writer%2Fpuremail/lists"}