socketmap-sql 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- {socketmap_sql-0.1.0.dist-info → socketmap_sql-0.2.1.dist-info}/METADATA +20 -17
- socketmap_sql-0.2.1.dist-info/RECORD +6 -0
- {socketmap_sql-0.1.0.dist-info → socketmap_sql-0.2.1.dist-info}/WHEEL +1 -2
- socketmap_sql-0.2.1.dist-info/entry_points.txt +3 -0
- socketmap_sql-0.2.1.dist-info/licenses/LICENSE +21 -0
- socketmapsql.py +278 -0
- socketmap_sql-0.1.0.data/scripts/socketmap-sql +0 -231
- socketmap_sql-0.1.0.dist-info/DESCRIPTION.rst +0 -78
- socketmap_sql-0.1.0.dist-info/RECORD +0 -7
- socketmap_sql-0.1.0.dist-info/metadata.json +0 -1
- socketmap_sql-0.1.0.dist-info/top_level.txt +0 -1
|
@@ -1,36 +1,41 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: socketmap-sql
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
License: MIT
|
|
9
|
-
Platform: UNKNOWN
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: An implementation of the sendmail socketmap protocol to allow an SQL database
|
|
5
|
+
Author-email: Keith Gaughan <k@stereochro.me>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/x-rst
|
|
8
|
+
License-Expression: MIT
|
|
10
9
|
Classifier: Development Status :: 4 - Beta
|
|
11
10
|
Classifier: Intended Audience :: System Administrators
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
11
|
Classifier: Operating System :: POSIX
|
|
14
12
|
Classifier: Programming Language :: Python :: 3
|
|
15
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
14
|
Classifier: Topic :: Communications :: Email :: Mail Transport Agents
|
|
17
15
|
Classifier: Topic :: Database
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Project-URL: Source, https://github.com/kgaughan/socketmap-sql
|
|
18
18
|
|
|
19
19
|
=============
|
|
20
20
|
socketmap-sql
|
|
21
21
|
=============
|
|
22
22
|
|
|
23
|
-
A
|
|
23
|
+
A socketmap_ script for interfacing with an SQL database.
|
|
24
|
+
|
|
25
|
+
.. _socketmap: http://www.postfix.org/socketmap_table.5.html
|
|
24
26
|
|
|
25
27
|
Why?
|
|
26
28
|
====
|
|
27
29
|
|
|
28
|
-
I have a number of
|
|
30
|
+
I have a number of FreeBSD_ servers, with one intended to act as my primary
|
|
29
31
|
mailserver (the current one and remaining one becoming secondary mailservers).
|
|
30
|
-
The problem is that I'm using
|
|
31
|
-
precompiled packages rather than using ports, and
|
|
32
|
+
The problem is that I'm using Postfix_, and trying very hard to stick to
|
|
33
|
+
precompiled packages rather than using ports, and Postfix on FreeBSD lacks
|
|
32
34
|
bindings to databases. In my case, I wanted to be able to use an SQL database.
|
|
33
35
|
|
|
36
|
+
.. _FreeBSD: https://www.freebsd.org/
|
|
37
|
+
.. _Postfix: http://www.postfix.org/
|
|
38
|
+
|
|
34
39
|
Configuration format
|
|
35
40
|
====================
|
|
36
41
|
|
|
@@ -47,7 +52,7 @@ function.
|
|
|
47
52
|
driver = sqlite3
|
|
48
53
|
database = /path/to/sqlite.db
|
|
49
54
|
|
|
50
|
-
Other sections start
|
|
55
|
+
Other sections start with ``table:``, and denote virtual tables to be queried.
|
|
51
56
|
There are two fields: *transform* (optional) and *query* (required).
|
|
52
57
|
|
|
53
58
|
The *transform* field gives the name of a transformation to apply to the query
|
|
@@ -88,9 +93,7 @@ following to ``master.cf``::
|
|
|
88
93
|
Compatibility
|
|
89
94
|
=============
|
|
90
95
|
|
|
91
|
-
The script only works on Python 3.
|
|
92
|
-
2.7 with some moderate patching.
|
|
96
|
+
The script only works on Python 3.8+.
|
|
93
97
|
|
|
94
98
|
.. vim:set ft=rst:
|
|
95
99
|
|
|
96
|
-
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
socketmapsql.py,sha256=ZcApgWGIGUZ1a81uCyhMUm-evcmrzxyHAlwjs_tpNB0,7316
|
|
2
|
+
socketmap_sql-0.2.1.dist-info/entry_points.txt,sha256=1N1u8xbsj9ryvD0v-HWBMxNfiCarn1Dq7pXXwjn7-8E,51
|
|
3
|
+
socketmap_sql-0.2.1.dist-info/licenses/LICENSE,sha256=Du-PK9kF9TBcjQfRGArxGPH9HVDUxifjzF5yFex2NKE,1070
|
|
4
|
+
socketmap_sql-0.2.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
5
|
+
socketmap_sql-0.2.1.dist-info/METADATA,sha256=iN1lE_1ACmwCwW3i1xaoczFdFsLa7lXNtVcZlvlKKOs,3229
|
|
6
|
+
socketmap_sql-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Keith Gaughan
|
|
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.
|
socketmapsql.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
An implementation of the sendmail socketmap protocol to allow an SQL database
|
|
4
|
+
to be queried out of process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import configparser
|
|
9
|
+
import contextlib
|
|
10
|
+
import dataclasses
|
|
11
|
+
import importlib
|
|
12
|
+
import os.path
|
|
13
|
+
import re
|
|
14
|
+
import select
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import typing as t
|
|
18
|
+
|
|
19
|
+
__version__: str = "0.2.1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Transform = t.Callable[[str, t.Mapping[str, str]], list[str]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class TableCfg:
|
|
27
|
+
query: str
|
|
28
|
+
transform: Transform
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Cfg(t.TypedDict):
|
|
32
|
+
database: dict[str, str]
|
|
33
|
+
misc: dict[str, str]
|
|
34
|
+
tables: dict[str, TableCfg]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
FUNC_REF_PATTERN = re.compile(
|
|
38
|
+
r"""
|
|
39
|
+
^
|
|
40
|
+
(?P<module>[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)
|
|
41
|
+
:
|
|
42
|
+
(?P<object>[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)
|
|
43
|
+
$
|
|
44
|
+
""",
|
|
45
|
+
re.IGNORECASE | re.VERBOSE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def match(name: str) -> tuple[str, str]:
|
|
50
|
+
matches = FUNC_REF_PATTERN.match(name)
|
|
51
|
+
if not matches:
|
|
52
|
+
raise ValueError(f"Malformed callable '{name}'")
|
|
53
|
+
return matches.group("module"), matches.group("object")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve(module_name: str, obj_name: str) -> t.Callable:
|
|
57
|
+
"""
|
|
58
|
+
Resolve a named object in a module.
|
|
59
|
+
"""
|
|
60
|
+
return getattr(importlib.import_module(module_name), obj_name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MalformedNetstringError(Exception):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def read_netstring(fp: t.IO[str]) -> str | None:
|
|
68
|
+
"""Reads a single netstring."""
|
|
69
|
+
ns = ""
|
|
70
|
+
while True:
|
|
71
|
+
c = fp.read(1)
|
|
72
|
+
if c == "":
|
|
73
|
+
return None
|
|
74
|
+
if c == ":":
|
|
75
|
+
break
|
|
76
|
+
if len(ns) > 10: # noqa: PLR2004
|
|
77
|
+
raise MalformedNetstringError
|
|
78
|
+
if c == "0" and ns == "":
|
|
79
|
+
# We can't allow leading zeros.
|
|
80
|
+
if fp.read(1) != ":":
|
|
81
|
+
raise MalformedNetstringError
|
|
82
|
+
ns = c
|
|
83
|
+
break
|
|
84
|
+
ns += c
|
|
85
|
+
n = int(ns, 10)
|
|
86
|
+
result = ""
|
|
87
|
+
while n > 0:
|
|
88
|
+
segment = fp.read(n)
|
|
89
|
+
if segment == "":
|
|
90
|
+
raise MalformedNetstringError
|
|
91
|
+
n -= len(segment)
|
|
92
|
+
result += segment
|
|
93
|
+
if fp.read(1) != ",":
|
|
94
|
+
raise MalformedNetstringError
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def write_netstring(fp: t.IO[str], response: str) -> None:
|
|
99
|
+
fp.write(f"{len(response)}:{response},")
|
|
100
|
+
fp.flush()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def process_local(local_part: str, cfg: t.Mapping[str, str]) -> str:
|
|
104
|
+
delimiter = cfg.get("recipient_delimiter", "").strip()
|
|
105
|
+
if delimiter != "":
|
|
106
|
+
local_part = local_part.split(delimiter, 1)[0]
|
|
107
|
+
return local_part.lower()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def split(arg: str, cfg: t.Mapping[str, str]) -> list[str]:
|
|
111
|
+
parts = arg.split("@", 1)
|
|
112
|
+
parts[0] = process_local(parts[0], cfg)
|
|
113
|
+
parts[1] = parts[1].lower()
|
|
114
|
+
return parts
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_config(fp: t.IO[str]) -> Cfg:
|
|
118
|
+
transforms = {
|
|
119
|
+
"all": lambda arg, _: [arg],
|
|
120
|
+
"lowercase": lambda arg, _: [arg.lower()],
|
|
121
|
+
"local": lambda arg, cfg: [process_local(arg.split("@", 1)[0], cfg)],
|
|
122
|
+
"domain": lambda arg, _: [arg.split("@", 1)[1].lower()],
|
|
123
|
+
"split": split,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cp = configparser.ConfigParser(interpolation=None)
|
|
127
|
+
cp.read_file(fp)
|
|
128
|
+
|
|
129
|
+
tables = {}
|
|
130
|
+
for section in cp.sections():
|
|
131
|
+
if section.startswith("table:"):
|
|
132
|
+
_, table_name = section.split(":", 1)
|
|
133
|
+
if not cp.has_option(section, "query"):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
transform_name = cp.get(section, "transform")
|
|
138
|
+
except configparser.NoOptionError:
|
|
139
|
+
transform_name = "all"
|
|
140
|
+
transform = transforms.get(transform_name)
|
|
141
|
+
if transform is None:
|
|
142
|
+
transform = resolve(*match(transform_name))
|
|
143
|
+
|
|
144
|
+
tables[table_name] = TableCfg(
|
|
145
|
+
query=cp.get(section, "query"),
|
|
146
|
+
transform=transform,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return Cfg(
|
|
150
|
+
database=dict(cp.items("database")),
|
|
151
|
+
tables=tables,
|
|
152
|
+
misc=dict(cp.items("misc")),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_int(cfg: t.Mapping[str, str], key: str) -> int | None:
|
|
157
|
+
return int(cfg[key]) if key in cfg else None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def serve_client(
|
|
161
|
+
fh_in: t.IO[str],
|
|
162
|
+
fh_out: t.IO[str],
|
|
163
|
+
conn,
|
|
164
|
+
timeout: int,
|
|
165
|
+
tables: t.Mapping[str, TableCfg],
|
|
166
|
+
cfg: t.Mapping[str, str],
|
|
167
|
+
) -> None:
|
|
168
|
+
max_requests = get_int(cfg, "max_requests")
|
|
169
|
+
try:
|
|
170
|
+
while True:
|
|
171
|
+
if max_requests is not None:
|
|
172
|
+
max_requests -= 1
|
|
173
|
+
if max_requests < 1:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
# Wait a short period before exiting.
|
|
177
|
+
iready, _, _ = select.select([fh_in], (), (), timeout)
|
|
178
|
+
if len(iready) == 0:
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
request = read_netstring(fh_in)
|
|
182
|
+
if request is None:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
table_name, arg = request.split(" ", 1)
|
|
186
|
+
|
|
187
|
+
table = tables.get(table_name)
|
|
188
|
+
if table is None:
|
|
189
|
+
write_netstring(fh_out, f"PERM no such table: {table_name}")
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
cur = conn.cursor()
|
|
193
|
+
try:
|
|
194
|
+
cur.execute(table.query, table.transform(arg, cfg))
|
|
195
|
+
result = cur.fetchone()
|
|
196
|
+
finally:
|
|
197
|
+
cur.close()
|
|
198
|
+
|
|
199
|
+
if result is None:
|
|
200
|
+
write_netstring(fh_out, "NOTFOUND ")
|
|
201
|
+
else:
|
|
202
|
+
write_netstring(fh_out, f"OK {result[0]!s}")
|
|
203
|
+
except MalformedNetstringError:
|
|
204
|
+
write_netstring(fh_out, "PERM malformed netstring")
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
write_netstring(fh_out, f"PERM {exc!s}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def connect(settings: dict[str, str]):
|
|
210
|
+
"""Connect to a database."""
|
|
211
|
+
driver = importlib.import_module(settings.pop("driver", "sqlite3"))
|
|
212
|
+
return driver.connect(**settings)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def make_parser() -> argparse.ArgumentParser:
|
|
216
|
+
parser = argparse.ArgumentParser(description="Database socketmap daemon.")
|
|
217
|
+
parser.add_argument(
|
|
218
|
+
"--config",
|
|
219
|
+
help="Path to config file",
|
|
220
|
+
type=argparse.FileType(),
|
|
221
|
+
default="/etc/socketmap-sql.ini",
|
|
222
|
+
)
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"--timeout",
|
|
225
|
+
help="Number of seconds to wait before exiting",
|
|
226
|
+
type=int,
|
|
227
|
+
default=5,
|
|
228
|
+
)
|
|
229
|
+
parser.add_argument(
|
|
230
|
+
"--client",
|
|
231
|
+
action="store_true",
|
|
232
|
+
help="Start with a debug client",
|
|
233
|
+
)
|
|
234
|
+
return parser
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def main() -> int:
|
|
238
|
+
args = make_parser().parse_args()
|
|
239
|
+
|
|
240
|
+
with contextlib.closing(args.config):
|
|
241
|
+
cfg = parse_config(args.config)
|
|
242
|
+
|
|
243
|
+
if args.client:
|
|
244
|
+
svr_args = [arg for arg in sys.argv if arg != "--client"]
|
|
245
|
+
svr_args[0] = os.path.abspath(svr_args[0])
|
|
246
|
+
proc = subprocess.Popen(
|
|
247
|
+
svr_args,
|
|
248
|
+
stdin=subprocess.PIPE,
|
|
249
|
+
stdout=subprocess.PIPE,
|
|
250
|
+
encoding="utf-8",
|
|
251
|
+
)
|
|
252
|
+
print("Type '.exit' to exit.")
|
|
253
|
+
while proc.poll() is None:
|
|
254
|
+
req = input("socketmap> ")
|
|
255
|
+
if req == ".exit":
|
|
256
|
+
proc.terminate()
|
|
257
|
+
break
|
|
258
|
+
if proc.stdin is not None and proc.stdout is not None:
|
|
259
|
+
write_netstring(proc.stdin, req)
|
|
260
|
+
print(read_netstring(proc.stdout))
|
|
261
|
+
else:
|
|
262
|
+
with contextlib.closing(connect(cfg["database"])) as conn:
|
|
263
|
+
serve_client(
|
|
264
|
+
sys.stdin,
|
|
265
|
+
sys.stdout,
|
|
266
|
+
conn,
|
|
267
|
+
args.timeout,
|
|
268
|
+
cfg["tables"],
|
|
269
|
+
cfg["misc"],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
sys.exit(main())
|
|
277
|
+
|
|
278
|
+
# vim:set ft=python et:
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
#!python
|
|
2
|
-
#
|
|
3
|
-
# socketmap-sql
|
|
4
|
-
# by Keith Gaughan (https://github.com/kgaughan/)
|
|
5
|
-
#
|
|
6
|
-
# This implements the sendmail socketmap protocol to allow an SQL database to
|
|
7
|
-
# be queried out of process.
|
|
8
|
-
#
|
|
9
|
-
# Copyright (c) Keith Gaughan, 2017. See 'LICENSE' for license details.
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
from __future__ import print_function
|
|
13
|
-
|
|
14
|
-
import argparse
|
|
15
|
-
import contextlib
|
|
16
|
-
try:
|
|
17
|
-
import configparser
|
|
18
|
-
except ImportError:
|
|
19
|
-
import ConfigParser as configparser
|
|
20
|
-
import importlib
|
|
21
|
-
import os.path
|
|
22
|
-
import re
|
|
23
|
-
import select
|
|
24
|
-
import subprocess
|
|
25
|
-
import sys
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
FUNC_REF_PATTERN = re.compile(r"""
|
|
29
|
-
^
|
|
30
|
-
(?P<module>
|
|
31
|
-
[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
|
|
32
|
-
)
|
|
33
|
-
:
|
|
34
|
-
(?P<object>
|
|
35
|
-
[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
|
|
36
|
-
)
|
|
37
|
-
$
|
|
38
|
-
""", re.I | re.X)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def match(name):
|
|
42
|
-
matches = FUNC_REF_PATTERN.match(name)
|
|
43
|
-
if not matches:
|
|
44
|
-
raise ValueError("Malformed callable '{}'".format(name))
|
|
45
|
-
return matches.group('module'), matches.group('object')
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def resolve(module_name, obj_name):
|
|
49
|
-
"""
|
|
50
|
-
Resolve a named object in a module.
|
|
51
|
-
"""
|
|
52
|
-
return getattr(importlib.import_module(module_name), obj_name)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class MalformedNetstringError(Exception):
|
|
56
|
-
pass
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def read_netstring(fp):
|
|
60
|
-
"""
|
|
61
|
-
Reads a single netstring.
|
|
62
|
-
"""
|
|
63
|
-
n = ""
|
|
64
|
-
while True:
|
|
65
|
-
c = fp.read(1)
|
|
66
|
-
if c == '':
|
|
67
|
-
return None
|
|
68
|
-
if c == ':':
|
|
69
|
-
break
|
|
70
|
-
if len(n) > 10:
|
|
71
|
-
raise MalformedNetstringError
|
|
72
|
-
if c == '0' and n == '':
|
|
73
|
-
# We can't allow leading zeros.
|
|
74
|
-
if fp.read(1) != ':':
|
|
75
|
-
raise MalformedNetstringError
|
|
76
|
-
n = c
|
|
77
|
-
break
|
|
78
|
-
n += c
|
|
79
|
-
n = int(n, 10)
|
|
80
|
-
result = ""
|
|
81
|
-
while n > 0:
|
|
82
|
-
segment = fp.read(n)
|
|
83
|
-
n -= len(segment)
|
|
84
|
-
result += segment
|
|
85
|
-
if fp.read(1) != ',':
|
|
86
|
-
raise MalformedNetstringError
|
|
87
|
-
return result
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def write_netstring(fp, response):
|
|
91
|
-
fp.write('{}:{},'.format(len(response), response))
|
|
92
|
-
fp.flush()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def parse_config(fp):
|
|
96
|
-
def passthrough(arg):
|
|
97
|
-
return [arg]
|
|
98
|
-
|
|
99
|
-
def local_part(arg):
|
|
100
|
-
return [arg.split('@', 1)[0]]
|
|
101
|
-
|
|
102
|
-
def domain_part(arg):
|
|
103
|
-
return [arg.split('@', 1)[1]]
|
|
104
|
-
|
|
105
|
-
def split(arg):
|
|
106
|
-
return arg.split('@', 1)
|
|
107
|
-
|
|
108
|
-
cp = configparser.RawConfigParser()
|
|
109
|
-
cp.readfp(fp)
|
|
110
|
-
|
|
111
|
-
result = {
|
|
112
|
-
'db': dict(cp.items('database')),
|
|
113
|
-
'tables': {},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for section in cp.sections():
|
|
117
|
-
if section.startswith('table:'):
|
|
118
|
-
_, table_name = section.split(':', 1)
|
|
119
|
-
if not cp.has_option(section, 'query'):
|
|
120
|
-
continue
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
transform_name = cp.get(section, 'transform')
|
|
124
|
-
except configparser.NoOptionError:
|
|
125
|
-
transform_name = 'all'
|
|
126
|
-
if transform_name == 'all':
|
|
127
|
-
transform = passthrough
|
|
128
|
-
elif transform_name == 'local':
|
|
129
|
-
transform = local_part
|
|
130
|
-
elif transform_name == 'domain':
|
|
131
|
-
transform = domain_part
|
|
132
|
-
elif transform_name == 'split':
|
|
133
|
-
transform = split
|
|
134
|
-
else:
|
|
135
|
-
transform = resolve(*match(transform_name))
|
|
136
|
-
|
|
137
|
-
result['tables'][table_name] = {
|
|
138
|
-
'transform': transform,
|
|
139
|
-
'query': cp.get(section, 'query'),
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return result
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def serve_client(fh_in, fh_out, conn, timeout, tables):
|
|
146
|
-
try:
|
|
147
|
-
while True:
|
|
148
|
-
# Wait a short period before exiting.
|
|
149
|
-
iready, _, _ = select.select([fh_in], (), (), timeout)
|
|
150
|
-
if len(iready) == 0:
|
|
151
|
-
break
|
|
152
|
-
|
|
153
|
-
request = read_netstring(fh_in)
|
|
154
|
-
|
|
155
|
-
table_name, arg = request.split(' ', 1)
|
|
156
|
-
|
|
157
|
-
table = tables.get(table_name)
|
|
158
|
-
if table is None:
|
|
159
|
-
write_netstring(fh_out,
|
|
160
|
-
'PERM no such table: ' + table_name)
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
cur = conn.cursor()
|
|
164
|
-
try:
|
|
165
|
-
cur.execute(table['query'], table['transform'](arg))
|
|
166
|
-
result = cur.fetchone()
|
|
167
|
-
finally:
|
|
168
|
-
cur.close()
|
|
169
|
-
|
|
170
|
-
if result is None:
|
|
171
|
-
write_netstring(fh_out, 'NOTFOUND ')
|
|
172
|
-
else:
|
|
173
|
-
write_netstring(fh_out, 'OK ' + str(result[0]))
|
|
174
|
-
except MalformedNetstringError:
|
|
175
|
-
write_netstring(fh_out, 'PERM malformed netstring')
|
|
176
|
-
except Exception as exc:
|
|
177
|
-
write_netstring(fh_out, 'PERM ' + str(exc))
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def connect(settings):
|
|
181
|
-
"""
|
|
182
|
-
Connect to a database.
|
|
183
|
-
"""
|
|
184
|
-
driver = importlib.import_module(settings.pop('driver', 'sqlite3'))
|
|
185
|
-
return driver.connect(**settings)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def main():
|
|
189
|
-
parser = argparse.ArgumentParser(description='Database socketmap daemon.')
|
|
190
|
-
parser.add_argument('--config',
|
|
191
|
-
help='Path to config file',
|
|
192
|
-
type=argparse.FileType(),
|
|
193
|
-
default='/etc/socketmap-sql.ini')
|
|
194
|
-
parser.add_argument('--timeout',
|
|
195
|
-
help='Number of seconds to wait before exiting',
|
|
196
|
-
type=int,
|
|
197
|
-
default=2)
|
|
198
|
-
parser.add_argument('--client', action='store_true',
|
|
199
|
-
help='Start with a debug client')
|
|
200
|
-
args = parser.parse_args()
|
|
201
|
-
|
|
202
|
-
with contextlib.closing(args.config):
|
|
203
|
-
cfg = parse_config(args.config)
|
|
204
|
-
|
|
205
|
-
if args.client:
|
|
206
|
-
svr_args = [arg for arg in sys.argv if arg != '--client']
|
|
207
|
-
svr_args[0] = os.path.abspath(svr_args[0])
|
|
208
|
-
proc = subprocess.Popen(svr_args,
|
|
209
|
-
stdin=subprocess.PIPE,
|
|
210
|
-
stdout=subprocess.PIPE,
|
|
211
|
-
encoding='utf-8')
|
|
212
|
-
print("Type '.exit' to exit.")
|
|
213
|
-
while proc.poll() is None:
|
|
214
|
-
req = input('socketmap> ')
|
|
215
|
-
if req == '.exit':
|
|
216
|
-
proc.terminate()
|
|
217
|
-
break
|
|
218
|
-
write_netstring(proc.stdin, req)
|
|
219
|
-
print(read_netstring(proc.stdout))
|
|
220
|
-
else:
|
|
221
|
-
with contextlib.closing(connect(cfg['db'])) as conn:
|
|
222
|
-
serve_client(sys.stdin, sys.stdout,
|
|
223
|
-
conn, args.timeout, cfg['tables'])
|
|
224
|
-
|
|
225
|
-
return 0
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if __name__ == '__main__':
|
|
229
|
-
sys.exit(main())
|
|
230
|
-
|
|
231
|
-
# vim:set ft=python et:
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
=============
|
|
2
|
-
socketmap-sql
|
|
3
|
-
=============
|
|
4
|
-
|
|
5
|
-
A socketmap script for interfacing with an SQL database.
|
|
6
|
-
|
|
7
|
-
Why?
|
|
8
|
-
====
|
|
9
|
-
|
|
10
|
-
I have a number of FreeBSD servers, with one intended to act as my primary
|
|
11
|
-
mailserver (the current one and remaining one becoming secondary mailservers).
|
|
12
|
-
The problem is that I'm using postfix, and trying very hard to stick to
|
|
13
|
-
precompiled packages rather than using ports, and postfix on FreeBSD lacks
|
|
14
|
-
bindings to databases. In my case, I wanted to be able to use an SQL database.
|
|
15
|
-
|
|
16
|
-
Configuration format
|
|
17
|
-
====================
|
|
18
|
-
|
|
19
|
-
Configuration files are INI files containing two types of section.
|
|
20
|
-
|
|
21
|
-
First is the ``[database]`` section, which gives database connection details.
|
|
22
|
-
The *driver* field specifies the driver to use; if omitted, its value defaults
|
|
23
|
-
to *sqlite3*. The remaining fields are passed to the driver's ``connect()``
|
|
24
|
-
function.
|
|
25
|
-
|
|
26
|
-
::
|
|
27
|
-
|
|
28
|
-
[database]
|
|
29
|
-
driver = sqlite3
|
|
30
|
-
database = /path/to/sqlite.db
|
|
31
|
-
|
|
32
|
-
Other sections start wth ``table:``, and denote virtual tables to be queried.
|
|
33
|
-
There are two fields: *transform* (optional) and *query* (required).
|
|
34
|
-
|
|
35
|
-
The *transform* field gives the name of a transformation to apply to the query
|
|
36
|
-
parameter before its use in the query the query. The default is to accept the
|
|
37
|
-
parameter as-is (*all*). Other values can be a reference to a Python function
|
|
38
|
-
in the form 'module:function', *local* for just the local part, *domain* for
|
|
39
|
-
the domain part of the address, and *split* breaks an email address in two.
|
|
40
|
-
It must return a list or tuple giving the postitional arguments to use in the
|
|
41
|
-
query.
|
|
42
|
-
|
|
43
|
-
The *query* field give an SQL query to be used to generate the synthetic table.
|
|
44
|
-
Use placeholders as specified by the database driver's documentation.
|
|
45
|
-
|
|
46
|
-
Usage
|
|
47
|
-
=====
|
|
48
|
-
|
|
49
|
-
Run with::
|
|
50
|
-
|
|
51
|
-
socketmap-sql --config /path/to/config.ini
|
|
52
|
-
|
|
53
|
-
If you don't provide the *--config* flag, it defaults to
|
|
54
|
-
``/etc/socketmap-sql.ini``.
|
|
55
|
-
|
|
56
|
-
Postfix
|
|
57
|
-
=======
|
|
58
|
-
|
|
59
|
-
This script is intended to be executed by Postfix's spawn_ mechanism, meaning
|
|
60
|
-
it reads its input and writes its output to stdin and stdout respectively.
|
|
61
|
-
|
|
62
|
-
.. _spawn: http://www.postfix.org/spawn.8.html
|
|
63
|
-
|
|
64
|
-
Assuming you've installed the script in ``/usr/local/libexec``, add the
|
|
65
|
-
following to ``master.cf``::
|
|
66
|
-
|
|
67
|
-
sockmapd unix - - n - 1 spawn
|
|
68
|
-
user=nobody argv=/usr/local/libexec/socketmap-sql
|
|
69
|
-
|
|
70
|
-
Compatibility
|
|
71
|
-
=============
|
|
72
|
-
|
|
73
|
-
The script only works on Python 3.3+, though it can be made to work on Python
|
|
74
|
-
2.7 with some moderate patching.
|
|
75
|
-
|
|
76
|
-
.. vim:set ft=rst:
|
|
77
|
-
|
|
78
|
-
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
socketmap_sql-0.1.0.data/scripts/socketmap-sql,sha256=0npExfWKFA2eWUb4inEd4LrKYuP9JMUYHd08MvO3Lx8,6183
|
|
2
|
-
socketmap_sql-0.1.0.dist-info/DESCRIPTION.rst,sha256=VjUam2HBajWb0iIyW8Mrx4u7ZAKrCULlqOXLH1OtQXQ,2460
|
|
3
|
-
socketmap_sql-0.1.0.dist-info/METADATA,sha256=MVvI2F3PVEhFchI6lvFRnvJbjxt6V15wplIIcGmlFdA,3092
|
|
4
|
-
socketmap_sql-0.1.0.dist-info/RECORD,,
|
|
5
|
-
socketmap_sql-0.1.0.dist-info/WHEEL,sha256=rNo05PbNqwnXiIHFsYm0m22u4Zm6YJtugFG2THx4w3g,92
|
|
6
|
-
socketmap_sql-0.1.0.dist-info/metadata.json,sha256=7Sk8EoCX0x_S0O6-TQ--YguY4x8gBwrMk9tYR119P8g,763
|
|
7
|
-
socketmap_sql-0.1.0.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Communications :: Email :: Mail Transport Agents", "Topic :: Database"], "extensions": {"python.details": {"contacts": [{"email": "k@stereochro.me", "name": "Keith Gaughan", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/kgaughan/socketmap-sql/"}}}, "generator": "bdist_wheel (0.29.0)", "license": "MIT", "metadata_version": "2.0", "name": "socketmap-sql", "summary": "A socketmap script for SQL databases", "version": "0.1.0"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|