telegram-sendmail 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. telegram_sendmail-1.0.0/.gitignore +44 -0
  2. telegram_sendmail-1.0.0/CHANGELOG.md +109 -0
  3. telegram_sendmail-1.0.0/CONTRIBUTING.md +283 -0
  4. telegram_sendmail-1.0.0/LICENSE +21 -0
  5. telegram_sendmail-1.0.0/PKG-INFO +372 -0
  6. telegram_sendmail-1.0.0/README.md +334 -0
  7. telegram_sendmail-1.0.0/SECURITY.md +97 -0
  8. telegram_sendmail-1.0.0/docs/assets/banner-dark.png +0 -0
  9. telegram_sendmail-1.0.0/docs/assets/banner.png +0 -0
  10. telegram_sendmail-1.0.0/pyproject.toml +137 -0
  11. telegram_sendmail-1.0.0/src/telegram_sendmail/__init__.py +13 -0
  12. telegram_sendmail-1.0.0/src/telegram_sendmail/__main__.py +480 -0
  13. telegram_sendmail-1.0.0/src/telegram_sendmail/client.py +201 -0
  14. telegram_sendmail-1.0.0/src/telegram_sendmail/config.py +496 -0
  15. telegram_sendmail-1.0.0/src/telegram_sendmail/exceptions.py +86 -0
  16. telegram_sendmail-1.0.0/src/telegram_sendmail/parser.py +435 -0
  17. telegram_sendmail-1.0.0/src/telegram_sendmail/py.typed +1 -0
  18. telegram_sendmail-1.0.0/src/telegram_sendmail/smtp.py +380 -0
  19. telegram_sendmail-1.0.0/src/telegram_sendmail/spool.py +112 -0
  20. telegram_sendmail-1.0.0/telegram-sendmail.ini.example +90 -0
  21. telegram_sendmail-1.0.0/tests/__init__.py +1 -0
  22. telegram_sendmail-1.0.0/tests/conftest.py +483 -0
  23. telegram_sendmail-1.0.0/tests/test_client.py +401 -0
  24. telegram_sendmail-1.0.0/tests/test_config.py +689 -0
  25. telegram_sendmail-1.0.0/tests/test_main.py +930 -0
  26. telegram_sendmail-1.0.0/tests/test_parser.py +528 -0
  27. telegram_sendmail-1.0.0/tests/test_smtp.py +575 -0
  28. telegram_sendmail-1.0.0/tests/test_spool.py +336 -0
@@ -0,0 +1,44 @@
1
+ # Build and distribution
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ *.egg
6
+
7
+ # PyInstaller / Nuitka binary artifacts
8
+ *.spec
9
+ *.onefile-build/
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+
14
+ # Virtual environments
15
+ .venv/
16
+ venv/
17
+ env/
18
+ ENV/
19
+
20
+ # Testing and coverage
21
+ .pytest_cache/
22
+ .coverage
23
+ coverage.json
24
+ coverage.xml
25
+ htmlcov/
26
+
27
+ # Quality tools cache
28
+ .mypy_cache/
29
+ .ruff_cache/
30
+
31
+ # Configuration files with secrets
32
+ *.ini
33
+
34
+ # Editor and OS artifacts
35
+ .idea/
36
+ .vscode/
37
+ *.swp
38
+ *.swo
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Logs and spools
43
+ *.log
44
+ /var/
@@ -0,0 +1,109 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-03-08
11
+
12
+ Production release.
13
+
14
+ ### Added
15
+
16
+ #### Drop-in sendmail compatibility
17
+
18
+ - Accepts all flags used by system daemons in practice: `-f`/`-r` (envelope
19
+ sender), `-s` (subject), `-bs` (SMTP server mode), `-t`, `-i`, `-oi`.
20
+ Positional recipient arguments (e.g. `sendmail root@localhost`) are consumed
21
+ silently; all mail is forwarded to the configured Telegram `chat_id`.
22
+ - **Pipe mode**: reads a raw RFC 2822 email from `stdin`, the standard
23
+ interface used by cron, logwatch, fail2ban, and the majority of Unix system
24
+ daemons.
25
+ - **SMTP server mode** (`-bs`): runs a minimal SMTP dialogue on
26
+ `stdin`/`stdout` without opening a network socket. Supported commands:
27
+ `EHLO`, `HELO`, `MAIL FROM`, `RCPT TO`, `DATA`, `RSET`, `NOOP`, `QUIT`.
28
+ Unrecognised commands return `250 Ok` for maximum daemon compatibility.
29
+ RFC 5321 dot-stuffing and null reverse-path (`MAIL FROM:<>`) are handled
30
+ correctly. A failed delivery attempt returns `554 Transaction failed` and
31
+ resets the session state; subsequent messages in the same session are
32
+ unaffected.
33
+
34
+ #### Zero-data-loss mail spooling
35
+
36
+ - Every raw email is written to a local spool file **before** any Telegram API
37
+ call is attempted. If the network is unavailable or delivery fails, the
38
+ original message is preserved on disk.
39
+ - The spool path defaults to `/var/mail/<username>`. If that directory is not
40
+ writable — common in containers — the daemon falls back to
41
+ `/tmp/.telegram-sendmail-spool/<username>` and emits a `WARNING` to syslog.
42
+ This hidden subdirectory is created on demand with `0700` DAC permissions,
43
+ preventing cross-user data exposure in the world-writable `/tmp` filesystem.
44
+ - A spool write failure is non-fatal: the message is still forwarded to
45
+ Telegram and the failure is surfaced in syslog at `WARNING` level.
46
+ - File permissions on the spool file are enforced to `0600` via `os.fchmod`
47
+ against the open file descriptor on every write, eliminating the TOCTOU race
48
+ condition that would otherwise exist in world-writable directories.
49
+
50
+ #### Resilient Telegram delivery
51
+
52
+ - Retriable responses (HTTP 429 and 5xx) cause the process to exit with
53
+ code `75` (`EX_TEMPFAIL`), signalling MTA-aware daemons to re-queue rather
54
+ than treat the message as permanently undeliverable.
55
+ - HTTP retries with exponential backoff on `429 Too Many Requests` and `5xx`
56
+ server errors. Retry count and backoff multiplier are operator-configurable.
57
+ - The Telegram API's `ok` field in the JSON response body is validated
58
+ independently of the HTTP status code, since the API can return HTTP 200
59
+ with `ok: false` for application-level errors such as an invalid `chat_id`.
60
+
61
+ #### HTML email support
62
+
63
+ - A two-pass sanitisation pipeline strips dangerous elements — including
64
+ `<script>`, `<style>`, `<iframe>`, `<object>`, `<embed>`, `<svg>`, and
65
+ `<noscript>` — and their entire subtrees before further processing.
66
+ Arbitrarily nested dangerous tags are handled correctly via depth tracking.
67
+ - Safe markup is converted to Telegram's supported HTML subset: `<b>`, `<i>`,
68
+ `<u>`, `<s>`, `<a>`, `<code>`, `<pre>`, `<blockquote>`. `javascript:` hrefs
69
+ are silently discarded.
70
+ - Plain-text parts are preferred; HTML is used as a fallback when no
71
+ `text/plain` part is present in the MIME structure.
72
+ - Multipart attachments are detected and a notice is appended to the Telegram
73
+ message. Attachment filenames are never disclosed.
74
+ - Long bodies are truncated at the nearest word boundary below the configured
75
+ limit; a visible truncation notice is appended.
76
+
77
+ #### Secure configuration
78
+
79
+ - Configuration is loaded from `/etc/telegram-sendmail.ini` (system-wide) or
80
+ `~/.telegram-sendmail.ini` (per-user). The user file takes precedence.
81
+ - Config files with group or world read bits emit a `WARNING` to syslog on
82
+ every startup. The check is advisory and does not block execution.
83
+ - Malformed or out-of-range values in `[options]` fall back to their defaults
84
+ with a `WARNING` logged; a single bad option never blocks delivery.
85
+ - Available options: `spool_dir`, `message_max_length`, `smtp_timeout`,
86
+ `telegram_timeout`, `max_retries`, `backoff_factor`, `disable_notification`.
87
+ - A fully-commented configuration template is provided as
88
+ `telegram-sendmail.ini.example`.
89
+
90
+ #### Structured logging
91
+
92
+ - All output goes to syslog under the `LOG_MAIL` facility with the identifier
93
+ `telegram-sendmail`, queryable via `journalctl -t telegram-sendmail`.
94
+ - `--console` flag routes log output to `stderr` for interactive debugging
95
+ and CI environments.
96
+ - `--debug` flag sets the log level to `DEBUG`, exposing SMTP command traces
97
+ and API request details.
98
+
99
+ #### Exit codes
100
+
101
+ | Code | Meaning |
102
+ |------|---------------------------------------------------------------|
103
+ | `0` | Message delivered successfully. |
104
+ | `1` | Operational failure (parse error, unrecoverable API error). |
105
+ | `75` | Transient failure — retriable error; daemon should retry. |
106
+ | `78` | Configuration error (missing file or missing required key). |
107
+
108
+ [Unreleased]: https://github.com/theodiv/telegram-sendmail/compare/v1.0.0...HEAD
109
+ [1.0.0]: https://github.com/theodiv/telegram-sendmail/releases/tag/v1.0.0
@@ -0,0 +1,283 @@
1
+ # Contributing to telegram-sendmail
2
+
3
+ Contributions are welcome. This document defines the standards, workflow, and
4
+ expectations for all submissions to this project.
5
+
6
+ ## Table of Contents
7
+
8
+ - [Code of Conduct](#code-of-conduct)
9
+ - [Getting Started](#getting-started)
10
+ - [Development Environment](#development-environment)
11
+ - [Toolchain & Quality Standards](#toolchain--quality-standards)
12
+ - [Running Tests](#running-tests)
13
+ - [Pull Request Process](#pull-request-process)
14
+ - [Reporting Issues](#reporting-issues)
15
+ - [Commit Message Convention](#commit-message-convention)
16
+
17
+ ## Code of Conduct
18
+
19
+ All interactions must remain professional and respectful. Harassment,
20
+ dismissive language, or personal attacks of any kind are not tolerated. Conduct
21
+ concerns may be raised via a private issue or by contacting the maintainer
22
+ directly through the contact information on the GitHub profile.
23
+
24
+ ## Getting Started
25
+
26
+ **Prerequisites:** Python 3.10 or later, `git`, and `pip`.
27
+
28
+ ```bash
29
+ # 1. Fork the repository on GitHub, then clone the fork
30
+ git clone https://github.com/<your-username>/telegram-sendmail.git
31
+ cd telegram-sendmail
32
+
33
+ # 2. Create an isolated virtual environment
34
+ python3 -m venv .venv
35
+ source .venv/bin/activate
36
+
37
+ # 3. Install the package in editable mode with all dev dependencies
38
+ pip install -e ".[dev]"
39
+
40
+ # 4. Install and activate the pre-commit hooks
41
+ pre-commit install
42
+ ```
43
+
44
+ Step 3 installs the package from the `src/` layout in editable mode so changes
45
+ to `src/telegram_sendmail/` are immediately reflected without reinstalling. The
46
+ `[dev]` extra installs the complete quality toolchain described below.
47
+
48
+ After this setup, every `git commit` will automatically run the full quality
49
+ pipeline on staged files via pre-commit.
50
+
51
+ ## Development Environment
52
+
53
+ The project uses the standard `src/` layout. Source files reside under
54
+ `src/telegram_sendmail/`. Tests reside under `tests/`. Source files must not
55
+ be placed outside the `src/` tree.
56
+
57
+ Key conventions:
58
+
59
+ - `src/telegram_sendmail/py.typed` is a PEP 561 marker file indicating that
60
+ this package ships inline type annotations.
61
+ - The authoritative version string is defined once in
62
+ `src/telegram_sendmail/__init__.py` (`__version__`). Hatchling reads it from
63
+ there. The version string must not be duplicated anywhere else in the
64
+ codebase.
65
+ - Configuration for all tools lives exclusively in `pyproject.toml`. Additional
66
+ tool-specific config files (`setup.cfg`, `tox.ini`, `.flake8`, etc.) are not
67
+ permitted.
68
+
69
+ The `[dev]` dependency group installs everything required for development:
70
+
71
+ | Tool | Purpose |
72
+ |-----------------|-----------------------------------------|
73
+ | `ruff` | Linting, formatting, and import sorting |
74
+ | `mypy` | Static type checking (strict mode) |
75
+ | `pytest` | Test runner |
76
+ | `pytest-cov` | Coverage measurement |
77
+ | `requests-mock` | HTTP transport-layer mocking |
78
+ | `pre-commit` | Git hook manager |
79
+
80
+ ### Binary builds — PyInstaller
81
+
82
+ The release pipeline compiles the package into a standalone binary using
83
+ [PyInstaller](https://pyinstaller.org/). Contributors working on imports,
84
+ entry points, or packaging must be aware of the following constraints:
85
+
86
+ - **Hidden imports:** modules imported dynamically (e.g. via
87
+ `importlib.import_module`) rather than through a static `import` statement
88
+ are not detected by PyInstaller's dependency analyser. A `--hidden-import`
89
+ flag or a `.spec` file hook is required for any such import.
90
+ - **Data files:** non-Python assets referenced at runtime (e.g. via
91
+ `importlib.resources` or path construction relative to `__file__`) must be
92
+ declared explicitly in the PyInstaller spec or they will not be bundled. The
93
+ current codebase has no such assets, but contributors adding them must
94
+ account for this.
95
+ - **`__file__` assumptions:** inside a frozen binary, `__file__` does not
96
+ point to a `.py` source file on disk. Any runtime path construction that
97
+ relies on `__file__` will behave differently inside the compiled binary
98
+ versus a standard Python installation. Use `importlib.resources` for
99
+ accessing package data instead.
100
+ - **Smoke-testing:** the release CI runs `telegram-sendmail --version` against
101
+ the compiled binary as a basic import sanity check. A PR that introduces an
102
+ import or packaging change that causes the binary to fail at startup will be
103
+ caught at this step.
104
+
105
+ To verify a local binary build before opening a PR that affects imports or
106
+ packaging:
107
+
108
+ ```bash
109
+ pip install pyinstaller
110
+ pyinstaller --onefile --name telegram-sendmail --strip \
111
+ src/telegram_sendmail/__main__.py
112
+ ./dist/telegram-sendmail --version
113
+ ```
114
+
115
+ ## Toolchain & Quality Standards
116
+
117
+ All toolchain configuration lives in `pyproject.toml`. The pre-commit pipeline
118
+ enforces these checks on every commit; CI enforces them on every push. A
119
+ contribution will not be merged if any check fails.
120
+
121
+ ### Linting & Formatting — Ruff
122
+
123
+ Ruff is the sole tool for formatting, linting, and import sorting.
124
+ The `pyproject.toml` configuration enables rule sets
125
+ including `pyflakes`, `pycodestyle`, `pyupgrade`, `pylint`, and
126
+ `flake8-pytest-style`.
127
+
128
+ ```bash
129
+ # Check for errors and auto-fix what is safe to fix automatically
130
+ ruff check --fix .
131
+
132
+ # Format all source files
133
+ ruff format .
134
+ ```
135
+
136
+ The pre-commit hook runs both commands automatically on staged files.
137
+
138
+ ### Type Checking — MyPy (Strict Mode)
139
+
140
+ All new code must pass MyPy under `strict` mode:
141
+
142
+ - Every function and method requires complete type annotations on parameters
143
+ and return values.
144
+ - `Any` is disallowed unless no viable alternative exists. If used, a comment
145
+ explaining the necessity is mandatory.
146
+ - `# type: ignore` comments are forbidden without a specific error code and a
147
+ documented rationale (e.g.,
148
+ `# type: ignore[misc] # html2text callback signature is untyped upstream`).
149
+
150
+ ```bash
151
+ mypy src/ tests/
152
+ ```
153
+
154
+ ### Running All Checks at Once
155
+
156
+ ```bash
157
+ pre-commit run --all-files
158
+ ```
159
+
160
+ ## Running Tests
161
+
162
+ Tests use `pytest` and reside in the `tests/` directory. Shared fixtures are
163
+ provided in `tests/conftest.py`. No test may make live network calls or write
164
+ to real filesystem paths outside `pytest`'s `tmp_path`.
165
+
166
+ ```bash
167
+ # Full test suite with coverage report
168
+ pytest
169
+
170
+ # Single module
171
+ pytest tests/test_parser.py
172
+
173
+ # Verbose output
174
+ pytest -v
175
+
176
+ # Keyword filter
177
+ pytest -k "smtp"
178
+ ```
179
+
180
+ The global coverage gate is set at **80%**. Critical logic in `client.py`,
181
+ `parser.py`, and `smtp.py` is enforced at **90%** by CI. New code should
182
+ meet or exceed these thresholds.
183
+
184
+ ### Test design standards
185
+
186
+ - Tests must exercise **real behaviour**, not mirror the implementation's
187
+ assumptions. A mock or fixture that encodes the same incorrect assumption
188
+ as the source code is a tautological test — it provides false confidence.
189
+ - Tests for filesystem operations (spool writes, config file reads) must use
190
+ `tmp_path` and must be skipped on UID 0 where they depend on DAC permission
191
+ enforcement, because root bypasses file permission checks on Linux.
192
+ - `requests-mock` is the standard mechanism for mocking HTTP calls. It patches
193
+ at the transport layer so the full `requests.Session` → `HTTPAdapter`
194
+ pipeline runs, except for the actual network call.
195
+ - Fixtures in `conftest.py` must be genuinely shared across multiple test
196
+ modules. Single-use fixtures — those consumed by only one test function or
197
+ one test class — must be defined locally in the relevant test module or as
198
+ a local helper within the test itself. Accumulating single-use fixtures in
199
+ `conftest.py` inflates the shared namespace, increases fixture discovery
200
+ overhead, and makes the file harder to reason about for contributors
201
+ unfamiliar with the codebase.
202
+
203
+ ## Pull Request Process
204
+
205
+ 1. **Branch from `main`** using a descriptive name:
206
+ `feat/retry-on-telegram-timeout`, `fix/smtp-dot-stuffing-edge-case`.
207
+
208
+ 2. **Keep PRs focused.** One logical change per PR. Unrelated improvements
209
+ observed while working on a fix should be submitted as separate PRs.
210
+
211
+ 3. **Update `CHANGELOG.md`** under the `[Unreleased]` section following the
212
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
213
+
214
+ 4. **Ensure the full pre-commit pipeline passes** before opening the PR:
215
+
216
+ ```bash
217
+ pre-commit run --all-files
218
+ pytest
219
+ ```
220
+
221
+ 5. **Write or update tests** for any logic added or modified.
222
+
223
+ 6. **Complete the PR description** — explain *what* changed and *why*. Link
224
+ to the relevant issue if one exists.
225
+
226
+ 7. A PR requires **one approval** from the maintainer before merging. Requested
227
+ changes should be addressed in new commits rather than force-pushes so the
228
+ review history is preserved.
229
+
230
+ 8. **Squash-merging** is the preferred strategy to maintain a linear `main` branch
231
+ history.
232
+
233
+ ## Reporting Issues
234
+
235
+ Before opening an issue, check the existing
236
+ [Issues](https://github.com/theodiv/telegram-sendmail/issues) to avoid
237
+ duplicates, and inspect `journalctl -t telegram-sendmail -f` for relevant
238
+ log output.
239
+
240
+ When opening an issue, include:
241
+
242
+ - The version (`telegram-sendmail --version`)
243
+ - Python version (`python3 --version`) and Linux distribution
244
+ - Installation method (pre-built binary or source)
245
+ - Whether the issue manifests in pipe mode or SMTP mode (`-bs`)
246
+ - Sanitised log output (bot token and chat ID must be removed before posting)
247
+ - Steps to reproduce
248
+
249
+ ## Commit Message Convention
250
+
251
+ This project follows the
252
+ [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
253
+ specification. Compliance is enforced automatically on every pull request.
254
+
255
+ ```
256
+ <type>(<scope>): <short summary>
257
+
258
+ [optional body]
259
+
260
+ [optional footer: Closes #<issue>]
261
+ ```
262
+
263
+ **Types:** `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`, `ci`.
264
+
265
+ **Scope** (optional): `config`, `parser`, `client`, `smtp`, `spool`, `cli`,
266
+ `build`.
267
+
268
+ **Examples:**
269
+
270
+ ```
271
+ feat(client): add exponential backoff retry on Telegram 429 responses
272
+
273
+ fix(smtp): handle bare LF in DATA stream without panicking
274
+
275
+ docs: document update-alternatives symlink strategy in README
276
+
277
+ build: add hidden-import hook for dynamic logging backend
278
+
279
+ chore: bump pre-commit hook revisions
280
+ ```
281
+
282
+ The short summary must use the imperative mood ("add", "fix", "remove") and
283
+ must not end with a period.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Theodosios Divolis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.