mailrows 0.1.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.
- mailrows-0.1.0/.gitattributes +4 -0
- mailrows-0.1.0/.github/workflows/publish.yml +71 -0
- mailrows-0.1.0/.github/workflows/tests.yml +33 -0
- mailrows-0.1.0/.gitignore +27 -0
- mailrows-0.1.0/CHANGELOG.md +35 -0
- mailrows-0.1.0/LICENSE +21 -0
- mailrows-0.1.0/PKG-INFO +321 -0
- mailrows-0.1.0/README.md +293 -0
- mailrows-0.1.0/SECURITY.md +42 -0
- mailrows-0.1.0/config.example.ini +87 -0
- mailrows-0.1.0/examples/events.csv +6 -0
- mailrows-0.1.0/examples/events.xlsx +0 -0
- mailrows-0.1.0/examples/template.html +13 -0
- mailrows-0.1.0/examples/template.txt +14 -0
- mailrows-0.1.0/pyproject.toml +56 -0
- mailrows-0.1.0/src/mailrows/__init__.py +3 -0
- mailrows-0.1.0/src/mailrows/__main__.py +5 -0
- mailrows-0.1.0/src/mailrows/cli.py +153 -0
- mailrows-0.1.0/src/mailrows/config.py +118 -0
- mailrows-0.1.0/src/mailrows/hooks/__init__.py +94 -0
- mailrows-0.1.0/src/mailrows/hooks/airtable.py +134 -0
- mailrows-0.1.0/src/mailrows/hooks/csv_append.py +44 -0
- mailrows-0.1.0/src/mailrows/hooks/http.py +103 -0
- mailrows-0.1.0/src/mailrows/hooks/webhook.py +66 -0
- mailrows-0.1.0/src/mailrows/ledger.py +210 -0
- mailrows-0.1.0/src/mailrows/mailer.py +205 -0
- mailrows-0.1.0/src/mailrows/run.py +569 -0
- mailrows-0.1.0/src/mailrows/smtpcheck.py +138 -0
- mailrows-0.1.0/src/mailrows/table.py +226 -0
- mailrows-0.1.0/src/mailrows/template.py +126 -0
- mailrows-0.1.0/tests/conftest.py +313 -0
- mailrows-0.1.0/tests/test_hooks.py +183 -0
- mailrows-0.1.0/tests/test_review_fixes.py +215 -0
- mailrows-0.1.0/tests/test_sending.py +265 -0
- mailrows-0.1.0/tests/test_units.py +118 -0
- mailrows-0.1.0/windows/1 check mail settings.bat +7 -0
- mailrows-0.1.0/windows/2 dry run.bat +7 -0
- mailrows-0.1.0/windows/3 send a test to me.bat +9 -0
- mailrows-0.1.0/windows/4 send for real.bat +7 -0
- mailrows-0.1.0/windows/_python.cmd +29 -0
- mailrows-0.1.0/windows/install.bat +30 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Runs when a GitHub release is published: tests, builds, and uploads to PyPI.
|
|
4
|
+
# Uses PyPI trusted publishing, so no API token is stored in the repository.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: true
|
|
17
|
+
matrix:
|
|
18
|
+
os: [ubuntu-latest, windows-latest]
|
|
19
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
20
|
+
runs-on: ${{ matrix.os }}
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
- uses: actions/setup-python@v7
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python }}
|
|
26
|
+
- name: Install
|
|
27
|
+
run: python -m pip install ".[test]"
|
|
28
|
+
- name: Test
|
|
29
|
+
run: python -m pytest -q
|
|
30
|
+
|
|
31
|
+
build:
|
|
32
|
+
needs: test
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v7
|
|
36
|
+
- uses: actions/setup-python@v7
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.13"
|
|
39
|
+
- name: Check the release tag matches the package version
|
|
40
|
+
shell: bash
|
|
41
|
+
run: |
|
|
42
|
+
version=$(grep -Po '__version__ = "\K[^"]+' src/mailrows/__init__.py)
|
|
43
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
44
|
+
if [ "$version" != "$tag" ]; then
|
|
45
|
+
echo "Release tag $GITHUB_REF_NAME does not match __version__ $version" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
- name: Build
|
|
49
|
+
run: |
|
|
50
|
+
python -m pip install build twine
|
|
51
|
+
python -m build
|
|
52
|
+
python -m twine check --strict dist/*
|
|
53
|
+
- uses: actions/upload-artifact@v7
|
|
54
|
+
with:
|
|
55
|
+
name: dist
|
|
56
|
+
path: dist/
|
|
57
|
+
|
|
58
|
+
publish:
|
|
59
|
+
needs: build
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
environment:
|
|
62
|
+
name: pypi
|
|
63
|
+
url: https://pypi.org/project/mailrows/
|
|
64
|
+
permissions:
|
|
65
|
+
id-token: write # lets PyPI verify this workflow; no token is stored
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/download-artifact@v8
|
|
68
|
+
with:
|
|
69
|
+
name: dist
|
|
70
|
+
path: dist/
|
|
71
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, windows-latest]
|
|
16
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
- uses: actions/setup-python@v7
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python }}
|
|
23
|
+
- name: Install
|
|
24
|
+
run: python -m pip install ".[test]"
|
|
25
|
+
- name: Test
|
|
26
|
+
run: python -m pytest -q
|
|
27
|
+
- name: Smoke test the installed command
|
|
28
|
+
shell: bash
|
|
29
|
+
run: |
|
|
30
|
+
cd "$RUNNER_TEMP"
|
|
31
|
+
mailrows init starter
|
|
32
|
+
mailrows send --dry-run -c starter/config.ini || test $? -eq 1
|
|
33
|
+
python -m mailrows --version
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# your settings and secrets: never commit these
|
|
2
|
+
config.ini
|
|
3
|
+
*.ini
|
|
4
|
+
!config.example.ini
|
|
5
|
+
.env
|
|
6
|
+
|
|
7
|
+
# send logs and anything mailrows writes while running
|
|
8
|
+
*.sqlite3
|
|
9
|
+
*.sqlite3-journal
|
|
10
|
+
sent-copy.csv
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*.egg-info/
|
|
16
|
+
build/
|
|
17
|
+
dist/
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
|
|
22
|
+
# editors and systems
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
~$*.xlsx
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/),
|
|
4
|
+
and versions follow [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-11-07
|
|
7
|
+
|
|
8
|
+
First release: mail merge from Excel or CSV through your own SMTP server, with a preview,
|
|
9
|
+
a typed confirmation, and a send log that never sends the same email twice.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `mailrows send`: one templated email per row of an `.xlsx` or `.csv` file, with a preview
|
|
13
|
+
table, a fully rendered sample, and a typed `yes` before anything is sent.
|
|
14
|
+
- `--dry-run` (changes nothing) and `--test-to ADDRESS` (everything to one address, `[TEST]`
|
|
15
|
+
subject, no files or logs changed).
|
|
16
|
+
- Row checks: valid address, required fields, unique IDs. Bad rows are listed and skipped.
|
|
17
|
+
- Templates with `{column}` placeholders, a `Subject:` first line and an optional HTML version.
|
|
18
|
+
Unknown placeholders stop the run before sending.
|
|
19
|
+
- SQLite send log keyed by row ID and message hash. Reruns never send twice, including after a
|
|
20
|
+
crash or a cleared status column. `mailrows resolve` for emails whose outcome is unknown.
|
|
21
|
+
- Post-send hooks: Airtable (records checked before sending, under 5 calls per second, retries
|
|
22
|
+
with backoff, "sent, update pending" state), webhook and CSV append.
|
|
23
|
+
- `mailrows smtp-check`: finds the working port and security setting without sending.
|
|
24
|
+
- `mailrows init`: a starter folder with a config, a sample sheet and templates.
|
|
25
|
+
- Settings from `config.ini` or `MAILROWS_*` environment variables.
|
|
26
|
+
- Double-click `.bat` helpers for Windows.
|
|
27
|
+
- `[input] status_save_every` to group status-column saves for large spreadsheets.
|
|
28
|
+
|
|
29
|
+
### Safety details
|
|
30
|
+
- Ctrl+C before an email starts going releases the row, so it isn't held as "may have been sent".
|
|
31
|
+
- A run stops after the mail server is unreachable twice in a row.
|
|
32
|
+
- HTTP redirects are never followed with credentials; webhook secrets require https.
|
|
33
|
+
- CSV files keep their original line endings when the status column is written.
|
|
34
|
+
|
|
35
|
+
[0.1.0]: https://github.com/munimh/mailrows/releases/tag/v0.1.0
|
mailrows-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The mailrows contributors
|
|
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.
|
mailrows-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mailrows
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mail merge from Excel or CSV through your own SMTP server: preview, confirm, never send twice.
|
|
5
|
+
Project-URL: Homepage, https://github.com/munimh/mailrows
|
|
6
|
+
Project-URL: Source, https://github.com/munimh/mailrows
|
|
7
|
+
Project-URL: Issues, https://github.com/munimh/mailrows/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/munimh/mailrows/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: airtable,bulk email,cli,csv,email,excel,mail merge,smtp,xlsx
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Communications :: Email
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: openpyxl>=3.0
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# mailrows
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/mailrows/)
|
|
32
|
+
[](https://pypi.org/project/mailrows/)
|
|
33
|
+
[](https://github.com/munimh/mailrows/blob/main/LICENSE)
|
|
34
|
+
[](https://github.com/munimh/mailrows/actions/workflows/tests.yml)
|
|
35
|
+
|
|
36
|
+
Mail merge from Excel or CSV in Python: send one templated email per row of a spreadsheet
|
|
37
|
+
through your own SMTP server, safely, from your own computer.
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
- **Preview first.** Every row is checked, a table shows who will get an email, one email is
|
|
42
|
+
shown in full, and nothing goes until you type `yes`.
|
|
43
|
+
- **Never twice.** A local send log records every email before and after it goes. Reruns skip
|
|
44
|
+
anything already sent, even if you cleared the status column, edited the sheet or the
|
|
45
|
+
process was killed halfway.
|
|
46
|
+
- **Bad rows are skipped, not sent.** Invalid addresses, empty required fields and duplicate
|
|
47
|
+
IDs are listed with the reason.
|
|
48
|
+
- **Test and dry-run modes** that change nothing.
|
|
49
|
+
- **Optional follow-up step** after each email: update an Airtable record, call a webhook, or
|
|
50
|
+
append to a CSV. If the email went but the update failed, only the update is retried.
|
|
51
|
+
- Plain Python 3.10+, one dependency (`openpyxl`). Windows, macOS and Linux.
|
|
52
|
+
|
|
53
|
+
## Quick start (5 minutes)
|
|
54
|
+
|
|
55
|
+
**1. Install** (needs Python 3.10 or newer):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pipx install mailrows # or: python -m pip install mailrows
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**2. Make a starter folder** with a config file, a sample sheet and a template:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mailrows init my-mailing
|
|
65
|
+
cd my-mailing
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**3. Fill in your mail server** in `config.ini`, under `[smtp]` and `[sender]`. Not sure which
|
|
69
|
+
port or security setting to use? Ask the tool; it tries the usual ones and sends nothing:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
mailrows smtp-check
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**4. See what would happen.** Nothing is sent and no file changes:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
mailrows send --dry-run
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The sample sheet has one row with a broken address on purpose, so you can see how bad rows
|
|
82
|
+
are reported.
|
|
83
|
+
|
|
84
|
+
**5. Send every email to yourself first**, marked `[TEST]`. No files or logs change:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
mailrows send --test-to your.name@example.com
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**6. Send for real.** Put your own rows in `events.csv` (or point `[input] file` at an `.xlsx`;
|
|
91
|
+
[`examples/events.xlsx`](https://github.com/munimh/mailrows/blob/main/examples/events.xlsx) is the same sample as a spreadsheet), then:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
mailrows send
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`python -m mailrows ...` works everywhere `mailrows ...` does.
|
|
98
|
+
|
|
99
|
+
### On Windows without a terminal
|
|
100
|
+
|
|
101
|
+
The `windows` folder has double-click helpers. Run `install.bat` once, copy the four numbered
|
|
102
|
+
`.bat` files and `_python.cmd` into the folder that holds your `config.ini`, then use
|
|
103
|
+
`1 check mail settings`, `2 dry run`, `3 send a test to me` and `4 send for real`.
|
|
104
|
+
|
|
105
|
+
## The spreadsheet
|
|
106
|
+
|
|
107
|
+
Any `.xlsx` or `.csv` file with a heading row. The config says which column is which:
|
|
108
|
+
|
|
109
|
+
```ini
|
|
110
|
+
[input]
|
|
111
|
+
file = events.csv
|
|
112
|
+
id_column = id ; unique per row, never reused
|
|
113
|
+
email_column = email
|
|
114
|
+
status_column = status ; mailrows writes progress here (optional)
|
|
115
|
+
required_columns = first_name, email
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Every column can be used in the template. The ID is what makes reruns safe, so give each row
|
|
119
|
+
an ID that stays with it (a booking number, a member number). Don't renumber rows.
|
|
120
|
+
|
|
121
|
+
## The template
|
|
122
|
+
|
|
123
|
+
A text file. The first line is the subject, the rest is the body. `{column name}` is replaced
|
|
124
|
+
with that row's value, matching the heading exactly. `{{` and `}}` give a literal brace.
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
Subject: Your place at the Riverside Maker Day, {first_name}
|
|
128
|
+
Hi {first_name},
|
|
129
|
+
|
|
130
|
+
You're booked into: {session}.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If the template uses a placeholder that isn't a column, the run stops before anything is sent
|
|
134
|
+
and tells you which one. For an HTML version too, set `[template] html = template.html`; it
|
|
135
|
+
uses the same placeholders, and values are HTML-escaped. The plain text is always included.
|
|
136
|
+
|
|
137
|
+
## What a run does
|
|
138
|
+
|
|
139
|
+
1. Reads the sheet and the template; stops on any unknown placeholder or missing column.
|
|
140
|
+
2. Checks every row: an ID, no duplicate IDs, a valid address, required fields filled, not
|
|
141
|
+
already sent. Problems are listed and those rows are skipped.
|
|
142
|
+
3. If a follow-up step is enabled, checks it can be done for every row (for Airtable, that the
|
|
143
|
+
matching record exists). Rows that fail the check are skipped.
|
|
144
|
+
4. Shows the preview table and one fully rendered email, then asks you to type `yes`.
|
|
145
|
+
5. Sends one by one, `delay_seconds` apart, writing the send log and the status column after
|
|
146
|
+
each email.
|
|
147
|
+
|
|
148
|
+
### Modes
|
|
149
|
+
|
|
150
|
+
| Command | Sends | Changes files or logs | Follow-up step |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `mailrows send` | to each row | yes | runs |
|
|
153
|
+
| `mailrows send --dry-run` | nothing | no | only its read-only check |
|
|
154
|
+
| `mailrows send --test-to ADDRESS` | all to ADDRESS, subject starts `[TEST]`, no BCC | no | skipped |
|
|
155
|
+
|
|
156
|
+
### How "never twice" works
|
|
157
|
+
|
|
158
|
+
The send log (`mailrows-log.sqlite3` next to your config) is the final word, not the
|
|
159
|
+
spreadsheet. For each row it keeps the row ID and a hash of the exact message (recipient,
|
|
160
|
+
subject and body).
|
|
161
|
+
|
|
162
|
+
- Before an email is handed to the server, a "sending" record is saved. After the server
|
|
163
|
+
accepts it, the record becomes "sent". Then the status column is updated.
|
|
164
|
+
- A rerun skips every row ID the log knows, whatever the status column says.
|
|
165
|
+
- It also skips a row whose message is identical to one already sent under another ID (for
|
|
166
|
+
example, a row copied with a new ID).
|
|
167
|
+
- If the server refuses an email, the record is removed and the next run tries again. The same
|
|
168
|
+
happens if you press Ctrl+C before the email has started going.
|
|
169
|
+
- If the mail server can't be reached twice in a row, the run stops instead of waiting on every
|
|
170
|
+
remaining row. Those rows go on the next run.
|
|
171
|
+
- If the process is killed, or the connection drops while the server is receiving the email,
|
|
172
|
+
the record stays "sending". mailrows can't know whether it arrived, so it doesn't guess: the
|
|
173
|
+
row is marked `check: may have been sent` and skipped. Look in your Sent folder, then:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
mailrows resolve E-1001 --sent # it arrived: never send it
|
|
177
|
+
mailrows resolve E-1001 --not-sent # it didn't: send it on the next run
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If the spreadsheet is open in Excel when a status needs writing, mailrows carries on (the log
|
|
181
|
+
already protects you) and the next run fixes the status column. For a very large `.xlsx`, set
|
|
182
|
+
`[input] status_save_every = 20` to save the status column every 20 emails instead of after
|
|
183
|
+
each one; the send log is still updated after every email. Close the file before a run to
|
|
184
|
+
avoid this; a real run refuses to start if it can't write to the sheet.
|
|
185
|
+
|
|
186
|
+
To send to a row again on purpose, give it a new ID.
|
|
187
|
+
|
|
188
|
+
## Settings
|
|
189
|
+
|
|
190
|
+
All settings live in `config.ini` (copy it from `config.example.ini`). The main ones:
|
|
191
|
+
|
|
192
|
+
```ini
|
|
193
|
+
[smtp]
|
|
194
|
+
host = smtp.example.com
|
|
195
|
+
port = 587
|
|
196
|
+
security = starttls ; ssl, starttls or none
|
|
197
|
+
username = your.name@example.com
|
|
198
|
+
password = CHANGE_ME ; or the MAILROWS_SMTP_PASSWORD environment variable
|
|
199
|
+
|
|
200
|
+
[sender]
|
|
201
|
+
from_name = Example Community Events
|
|
202
|
+
from_email = events@example.com
|
|
203
|
+
reply_to = hello@example.com
|
|
204
|
+
bcc = ; optional; real sends only, never shown to recipients
|
|
205
|
+
delay_seconds = 2
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Any setting can come from an environment variable named `MAILROWS_<SECTION>_<KEY>`, which wins
|
|
209
|
+
over the file: `MAILROWS_SMTP_PASSWORD`, `MAILROWS_AIRTABLE_TOKEN`, and so on.
|
|
210
|
+
|
|
211
|
+
## Follow-up steps (hooks)
|
|
212
|
+
|
|
213
|
+
Off by default. Turn them on with a comma-separated list:
|
|
214
|
+
|
|
215
|
+
```ini
|
|
216
|
+
[hooks]
|
|
217
|
+
enabled = airtable
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Airtable
|
|
221
|
+
|
|
222
|
+
Updates one record per sent email.
|
|
223
|
+
|
|
224
|
+
```ini
|
|
225
|
+
[airtable]
|
|
226
|
+
base_id = appXXXXXXXXXXXXXX
|
|
227
|
+
table = Attendees ; name or ID
|
|
228
|
+
match_field = Ticket ID ; the Airtable field to find the record by
|
|
229
|
+
match_column = id ; the spreadsheet column with the same value
|
|
230
|
+
token = CHANGE_ME ; or MAILROWS_AIRTABLE_TOKEN
|
|
231
|
+
|
|
232
|
+
[airtable.fields]
|
|
233
|
+
Invite status = Invited ; a fixed value
|
|
234
|
+
Session = {session} ; from the row
|
|
235
|
+
Invite sent on = {_sent_date}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- Every record is looked up **before any email is sent**. A row with no matching record, or
|
|
239
|
+
more than one, is skipped and listed. A bad token or table name stops the run.
|
|
240
|
+
- Calls stay under 5 per second and are retried with backoff when Airtable is busy.
|
|
241
|
+
- If an email went but its update failed, the row is marked `sent, update pending`. The next
|
|
242
|
+
run retries only the update. The email is never sent again.
|
|
243
|
+
|
|
244
|
+
Built-in values for any hook: `{_sent_at}`, `{_sent_date}`, `{_row_id}`, `{_recipient}`,
|
|
245
|
+
`{_subject}`.
|
|
246
|
+
|
|
247
|
+
Create the token at airtable.com/create/tokens with the `data.records:read` and
|
|
248
|
+
`data.records:write` scopes, limited to the one base.
|
|
249
|
+
|
|
250
|
+
### Webhook
|
|
251
|
+
|
|
252
|
+
Posts `{"row_id", "recipient", "subject", "sent_at", "row": {...}}` as JSON after each email.
|
|
253
|
+
|
|
254
|
+
```ini
|
|
255
|
+
[webhook]
|
|
256
|
+
url = https://hooks.example.com/mailrows
|
|
257
|
+
header_name = X-Mailrows-Token ; optional shared secret
|
|
258
|
+
header_value = CHANGE_ME ; or MAILROWS_WEBHOOK_HEADER_VALUE
|
|
259
|
+
columns = id, email ; default: all columns
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
A retry can deliver the same row twice, so treat `row_id` as an idempotency key.
|
|
263
|
+
|
|
264
|
+
### CSV append
|
|
265
|
+
|
|
266
|
+
```ini
|
|
267
|
+
[csv_append]
|
|
268
|
+
path = sent-copy.csv
|
|
269
|
+
columns = id, email, _sent_at
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Writing your own
|
|
273
|
+
|
|
274
|
+
Subclass `mailrows.hooks.Hook`: `prepare(rows)` runs before any email and returns the rows it
|
|
275
|
+
can't handle; `run(row)` runs after each email and raises on failure. Register it in
|
|
276
|
+
`load_hooks`. The pending/retry bookkeeping is handled for you.
|
|
277
|
+
|
|
278
|
+
## smtp-check
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
mailrows smtp-check # uses [smtp] from config.ini
|
|
282
|
+
mailrows smtp-check --host smtp.example.com --username your.name@example.com
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Tries port 465 with SSL, 587 with STARTTLS, 25 and 2525, connects, tests the login without
|
|
286
|
+
sending, and tells you which `port` and `security` to use. It never sends a password over an
|
|
287
|
+
unencrypted connection. If every port times out, something on your computer or network (a
|
|
288
|
+
firewall, an antivirus "mail shield", an office or hotel network) is blocking outgoing mail.
|
|
289
|
+
|
|
290
|
+
## Why not Zapier or Make?
|
|
291
|
+
|
|
292
|
+
They are great for many things, but for "email everyone on this list":
|
|
293
|
+
|
|
294
|
+
- **Cost.** Each row is a billed task. A few hundred emails a month can need a paid plan.
|
|
295
|
+
- **No real preview.** You find out what the 200th email looks like after it's sent.
|
|
296
|
+
- **Reruns are risky.** Replaying a failed run can email people twice; a half-finished run is
|
|
297
|
+
hard to pick up safely.
|
|
298
|
+
- **Your data stays with you.** The spreadsheet, the addresses and the send log stay on your
|
|
299
|
+
computer, and mail goes straight from your own mail server.
|
|
300
|
+
|
|
301
|
+
mailrows is not a newsletter platform: there is no unsubscribe handling, open tracking or
|
|
302
|
+
bounce processing. For marketing email to large lists, use a service built for that.
|
|
303
|
+
|
|
304
|
+
## Exit codes
|
|
305
|
+
|
|
306
|
+
`0` everything went, `1` some rows had problems (listed), `2` stopped before sending (setup
|
|
307
|
+
problem), `3` cancelled at the prompt.
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
python -m pip install -e ".[test]"
|
|
313
|
+
python -m pytest
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The tests run against a fake SMTP server and a fake Airtable API on `127.0.0.1`; nothing
|
|
317
|
+
leaves your machine.
|
|
318
|
+
|
|
319
|
+
## Licence
|
|
320
|
+
|
|
321
|
+
MIT. See [LICENSE](https://github.com/munimh/mailrows/blob/main/LICENSE).
|