feat(init): First commit
This commit is contained in:
commit
f92368748a
22 changed files with 1298 additions and 0 deletions
5
.env
Normal file
5
.env
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export PGHOST=localhost
|
||||||
|
export PGPORT=5432
|
||||||
|
export PGDATABASE=vmail
|
||||||
|
export PGUSER=postgres
|
||||||
|
export PGPASSWORD=postgres123
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mailout.yml
|
69
README.md
Normal file
69
README.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# mailout
|
||||||
|
|
||||||
|
This is a simple binary to handle your self hosted mail notification-sending machines.
|
||||||
|
|
||||||
|
**this is not intended for human users**
|
||||||
|
|
||||||
|
The premise is as follow:
|
||||||
|
|
||||||
|
* you authenticate users with dovecot
|
||||||
|
* you sign emails with opendkim
|
||||||
|
* you use postfix (might work in other cases but havent tried)
|
||||||
|
|
||||||
|
The plan is basically:
|
||||||
|
* you have one table to handle your users and their passwords
|
||||||
|
* you have one for the DKIM keys you use
|
||||||
|
* you have a view to expose the relevant key to use for opendkim
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
I halfed assed that shit on an afternoon to suit my own needs, improvements will follow.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Create mail users
|
||||||
|
* Deletes them
|
||||||
|
* Update passwords
|
||||||
|
|
||||||
|
* Create DKIM keys
|
||||||
|
* Delete them
|
||||||
|
* Publish them to a DNS provider (currently only OVH)
|
||||||
|
* Unpublish them to a DNS provider (currently only OVH)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
* Postgres with a user
|
||||||
|
* Optionally an OVH account
|
||||||
|
|
||||||
|
### Dovecot
|
||||||
|
For authentication, you want to change your `/etc/dovecot/dovecot-sql.conf.ext` to
|
||||||
|
```
|
||||||
|
driver = pgsql
|
||||||
|
connect = host=YOURHOST port=5432 user=YOURUSER password=APASSWORD dbname=DBNAME
|
||||||
|
password_query = SELECT password, username AS user FROM users WHERE username = '%n' AND domain = '%d'
|
||||||
|
user_query = SELECT maildir, 1000 AS uid, 1000 AS gid FROM users WHERE username = '%n' AND domain = '%d' AND active = '1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenDKIM
|
||||||
|
You want to change the `SigningTable` and `KeyTable` settings in `opendkim.conf` to
|
||||||
|
```
|
||||||
|
SigningTable dsn:pgsql://USER:PASSWORD@HOST/DATABASE/table=signing_table?keycol=domain_name?datacol=id
|
||||||
|
KeyTable dsn:pgsql://USER:PASSWORD@HOST/DATABASE/table=dkimkeys?keycol=id?datacol=domain_name,selector,private_key
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you should be good to go, fill yourself a config file like that
|
||||||
|
```yaml
|
||||||
|
postgres:
|
||||||
|
hostname: localhost
|
||||||
|
port: 5432
|
||||||
|
database: vmail
|
||||||
|
user: postgres
|
||||||
|
password: postgres123
|
||||||
|
sslmode: disable
|
||||||
|
providers:
|
||||||
|
ovh:
|
||||||
|
application_key: <CHANGEME>
|
||||||
|
application_secret: <CHANGEME>
|
||||||
|
consumer_key: <CHANGEME>
|
||||||
|
endpoint: ovh-eu
|
||||||
|
```
|
||||||
|
|
||||||
|
pointing to your DB and you're good
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 5432:5432/tcp
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres123
|
||||||
|
- POSTGRES_DB=vmail
|
||||||
|
volumes:
|
||||||
|
- postgres:/var/lib/postgresql/15/main
|
||||||
|
volumes:
|
||||||
|
postgres: {}
|
39
go.mod
Normal file
39
go.mod
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
module git.maurice.fr/thomas/mailout
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/pterm/pterm v0.12.79
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
|
golang.org/x/crypto v0.14.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/postgres v1.5.6
|
||||||
|
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
atomicgo.dev/cursor v0.2.0 // indirect
|
||||||
|
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||||
|
atomicgo.dev/schedule v0.1.0 // indirect
|
||||||
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
|
github.com/gookit/color v1.5.4 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/ovh/go-ovh v1.4.3 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
|
golang.org/x/term v0.16.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
)
|
144
go.sum
Normal file
144
go.sum
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
|
||||||
|
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
|
||||||
|
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||||
|
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||||
|
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||||
|
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||||
|
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||||
|
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||||
|
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||||
|
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||||
|
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||||
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||||
|
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||||
|
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||||
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0=
|
||||||
|
github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||||
|
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||||
|
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||||
|
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||||
|
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||||
|
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||||
|
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||||
|
github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4=
|
||||||
|
github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU=
|
||||||
|
gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
|
||||||
|
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
|
||||||
|
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
13
mailout.yml.sample
Normal file
13
mailout.yml.sample
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
postgres:
|
||||||
|
hostname: localhost
|
||||||
|
port: 5432
|
||||||
|
database: vmail
|
||||||
|
user: postgres
|
||||||
|
password: postgres123
|
||||||
|
sslmode: disable
|
||||||
|
providers:
|
||||||
|
ovh:
|
||||||
|
application_key: <CHANGEME>
|
||||||
|
application_secret: <CHANGEME>
|
||||||
|
consumer_key: <CHANGEME>
|
||||||
|
endpoint: ovh-eu
|
14
main.go
Normal file
14
main.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.InitRootCmd()
|
||||||
|
if err := cmd.RootCmd.Execute(); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
417
pkg/cmd/dkimkey.go
Normal file
417
pkg/cmd/dkimkey.go
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/crypto"
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/database"
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/models"
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/providers/ovh"
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/utils"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pterm/pterm"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagDKIMKeyActive bool
|
||||||
|
flagSelector string
|
||||||
|
flagKeyBits int
|
||||||
|
)
|
||||||
|
|
||||||
|
var DKIMKeyCmd = &cobra.Command{
|
||||||
|
Use: "dkimkey",
|
||||||
|
Short: "manages DKIM keys",
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyAddCmd = &cobra.Command{
|
||||||
|
Use: "add [domain]",
|
||||||
|
Short: "adds a DKIM signing key",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, pub, err := crypto.GenerateKeyPair(flagKeyBits)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selector := flagSelector
|
||||||
|
if selector == "" {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal("entropy generator is kill")
|
||||||
|
}
|
||||||
|
|
||||||
|
selector = hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
dkimkey := models.DKIMKey{
|
||||||
|
DomainName: args[0],
|
||||||
|
PublicKey: pub,
|
||||||
|
PrivateKey: priv,
|
||||||
|
Active: flagUserActive,
|
||||||
|
Selector: selector,
|
||||||
|
Record: fmt.Sprintf("%s._domainkey.%s. IN TXT \"v=DKIM1; k=rsa; p=%s\"", selector, args[0], pub),
|
||||||
|
}
|
||||||
|
err = db.Save(&dkimkey).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not create DKIM key")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("created DKIM key %s", dkimkey.ID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "list DKIM keys",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
qRes := make([]models.DKIMKey, 0)
|
||||||
|
|
||||||
|
err = db.Find(&qRes).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not list keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
tData := pterm.TableData{
|
||||||
|
{"id", "selector", "domain", "active", "created_at", "updated_at"},
|
||||||
|
}
|
||||||
|
for _, k := range qRes {
|
||||||
|
tData = append(tData, []string{k.ID.String(), k.Selector, k.DomainName, fmt.Sprintf("%v", k.Active), k.CreatedAt.String(), k.UpdatedAt.String()})
|
||||||
|
}
|
||||||
|
|
||||||
|
pterm.DefaultTable.WithHasHeader().WithData(tData).Render()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyRecordCmd = &cobra.Command{
|
||||||
|
Use: "record",
|
||||||
|
Short: "gives the dns record for a key",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
qRes := make([]models.DKIMKey, 0)
|
||||||
|
|
||||||
|
id, err := uuid.Parse(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not search keys")
|
||||||
|
}
|
||||||
|
if len(qRes) == 0 {
|
||||||
|
logrus.WithError(err).Fatal("No such key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(qRes[0].Record)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyDeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete",
|
||||||
|
Short: "delete DKIM keys",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("invalid UUID")
|
||||||
|
}
|
||||||
|
err = db.Where(&models.DKIMKey{ID: id}).Delete(&models.DKIMKey{}).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not delete DKIM key")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("deleted DKIM key %s if it existed", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyActivateCmd = &cobra.Command{
|
||||||
|
Use: "activate",
|
||||||
|
Short: "activates a DKIM key",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&models.DKIMKey{}).Where(&models.DKIMKey{ID: id}).Update("active", true).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not activate DKIM key")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("activated DKIM key %s", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyDeactivateCmd = &cobra.Command{
|
||||||
|
Use: "deactivate",
|
||||||
|
Short: "deactivates a DKIM key",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&models.DKIMKey{}).Where(&models.DKIMKey{ID: id}).Update("active", false).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not deactivate DKIM key")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("deactivated DKIM key %s", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyPublishCmd = &cobra.Command{
|
||||||
|
Use: "publish",
|
||||||
|
Short: "publishes a DKIM key onto a DNS provider",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
qRes := make([]models.DKIMKey, 0)
|
||||||
|
|
||||||
|
id, err := uuid.Parse(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not get key")
|
||||||
|
}
|
||||||
|
if len(qRes) == 0 {
|
||||||
|
logrus.WithError(err).Fatal("No such key")
|
||||||
|
}
|
||||||
|
|
||||||
|
dkimkey := qRes[0]
|
||||||
|
logger := logrus.WithFields(logrus.Fields{
|
||||||
|
"dkimkey": dkimkey.ID,
|
||||||
|
"selector": dkimkey.Selector,
|
||||||
|
})
|
||||||
|
|
||||||
|
ovhClient, err := ovh.NewOVHProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not get OVH DNS provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
zone, err := utils.GetZone(dkimkey.DomainName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not determine zone")
|
||||||
|
}
|
||||||
|
subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not determine subdomain")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subdomain == "" {
|
||||||
|
subdomain = zone
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = logger.WithFields(logrus.Fields{
|
||||||
|
"zone": zone,
|
||||||
|
"subdomain": subdomain,
|
||||||
|
})
|
||||||
|
|
||||||
|
dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
|
||||||
|
result := make([]int, 0)
|
||||||
|
|
||||||
|
err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not lookup records")
|
||||||
|
}
|
||||||
|
|
||||||
|
type createParams struct {
|
||||||
|
FieldType string `json:"fieldType"`
|
||||||
|
SubDomain string `json:"subDomain"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateParams struct {
|
||||||
|
SubDomain string `json:"subDomain"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
logger.Info("no DKIM records found, creating a new one")
|
||||||
|
|
||||||
|
c := createParams{
|
||||||
|
FieldType: "TXT",
|
||||||
|
SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
|
||||||
|
Target: fmt.Sprintf("\"v=DKIM1; k=rsa; p=%s\"", dkimkey.PublicKey),
|
||||||
|
TTL: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/record", zone), &c, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not create new record")
|
||||||
|
}
|
||||||
|
logger.Info("created new DKIM record")
|
||||||
|
|
||||||
|
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not refresh the zone")
|
||||||
|
}
|
||||||
|
logger.Info("refreshed zone")
|
||||||
|
} else if len(result) == 1 {
|
||||||
|
logger.Info("found one record, updating it")
|
||||||
|
|
||||||
|
u := updateParams{
|
||||||
|
SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
|
||||||
|
Target: fmt.Sprintf("\"v=DKIM1; k=foo; p=%s\"", dkimkey.PublicKey),
|
||||||
|
TTL: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ovhClient.Client.Put(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), &u, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not update record")
|
||||||
|
}
|
||||||
|
logger.Info("updated existing record")
|
||||||
|
|
||||||
|
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not refresh the zone")
|
||||||
|
}
|
||||||
|
logger.Info("refreshed zone")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DKIMKeyUnpublishCmd = &cobra.Command{
|
||||||
|
Use: "unpublish",
|
||||||
|
Short: "removes a DKIM key onto a DNS provider",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
qRes := make([]models.DKIMKey, 0)
|
||||||
|
|
||||||
|
id, err := uuid.Parse(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not get key")
|
||||||
|
}
|
||||||
|
if len(qRes) == 0 {
|
||||||
|
logrus.WithError(err).Fatal("No such key")
|
||||||
|
}
|
||||||
|
|
||||||
|
dkimkey := qRes[0]
|
||||||
|
logger := logrus.WithFields(logrus.Fields{
|
||||||
|
"dkimkey": dkimkey.ID,
|
||||||
|
"selector": dkimkey.Selector,
|
||||||
|
})
|
||||||
|
|
||||||
|
ovhClient, err := ovh.NewOVHProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not get OVH DNS provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
zone, err := utils.GetZone(dkimkey.DomainName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not determine zone")
|
||||||
|
}
|
||||||
|
subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not determine subdomain")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subdomain == "" {
|
||||||
|
subdomain = zone
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = logger.WithFields(logrus.Fields{
|
||||||
|
"zone": zone,
|
||||||
|
"subdomain": subdomain,
|
||||||
|
})
|
||||||
|
|
||||||
|
dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
|
||||||
|
result := make([]int, 0)
|
||||||
|
|
||||||
|
err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not lookup records")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
logger.Info("no DKIM records found, no need to do anything")
|
||||||
|
} else if len(result) == 1 {
|
||||||
|
logger.Info("found one record, deleting it")
|
||||||
|
|
||||||
|
err = ovhClient.Client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not delete record")
|
||||||
|
}
|
||||||
|
logger.Info("deleted existing record")
|
||||||
|
|
||||||
|
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not refresh the zone")
|
||||||
|
}
|
||||||
|
logger.Info("refreshed zone")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDKIMKeyCmd() {
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyAddCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyListCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyDeleteCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyActivateCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyDeactivateCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyRecordCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyPublishCmd)
|
||||||
|
DKIMKeyCmd.AddCommand(DKIMKeyUnpublishCmd)
|
||||||
|
|
||||||
|
DKIMKeyAddCmd.PersistentFlags().BoolVarP(&flagDKIMKeyActive, "active", "a", true, "whether or not the created key is active")
|
||||||
|
DKIMKeyAddCmd.PersistentFlags().StringVarP(&flagSelector, "selector", "s", "", "force a selector for the key")
|
||||||
|
DKIMKeyAddCmd.PersistentFlags().IntVarP(&flagKeyBits, "key-bits", "k", 2048, "force a size for the key")
|
||||||
|
}
|
25
pkg/cmd/initdb.go
Normal file
25
pkg/cmd/initdb.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/database"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var InitDBCmd = &cobra.Command{
|
||||||
|
Use: "initdb",
|
||||||
|
Short: "initialises the database",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.InitMigrate(db)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not initialise the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("successfully initialised the database")
|
||||||
|
},
|
||||||
|
}
|
48
pkg/cmd/root.go
Normal file
48
pkg/cmd/root.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configFile string
|
||||||
|
cfg *config.Config
|
||||||
|
)
|
||||||
|
|
||||||
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "mailoutctl",
|
||||||
|
Short: "mailout management utility",
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
var err error
|
||||||
|
cfg, err = config.LoadConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var VersionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print the version number",
|
||||||
|
Long: ``,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Println("v0.0.1")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitRootCmd() {
|
||||||
|
InitUserCmd()
|
||||||
|
InitDKIMKeyCmd()
|
||||||
|
|
||||||
|
RootCmd.AddCommand(VersionCmd)
|
||||||
|
RootCmd.AddCommand(InitDBCmd)
|
||||||
|
RootCmd.AddCommand(UserCmd)
|
||||||
|
RootCmd.AddCommand(DKIMKeyCmd)
|
||||||
|
|
||||||
|
RootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "mailout.yml", "Configuration file")
|
||||||
|
}
|
193
pkg/cmd/user.go
Normal file
193
pkg/cmd/user.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/database"
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/models"
|
||||||
|
"github.com/pterm/pterm"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagUserActive bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var UserCmd = &cobra.Command{
|
||||||
|
Use: "user",
|
||||||
|
Short: "manages users",
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserAddCmd = &cobra.Command{
|
||||||
|
Use: "add [user_address] [password]",
|
||||||
|
Short: "adds a user",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
username, domain, err := splitUser(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not parse user")
|
||||||
|
}
|
||||||
|
|
||||||
|
qRes := make([]models.User, 0)
|
||||||
|
var user models.User
|
||||||
|
userExists := false
|
||||||
|
logger := logrus.WithField("user", args[0])
|
||||||
|
|
||||||
|
err = db.Where(&models.User{Domain: domain, Username: username}).Find(&qRes).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not check user's existence")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(qRes) == 0 {
|
||||||
|
userExists = false
|
||||||
|
} else {
|
||||||
|
logger.Warning("user already exists, it's password will be updated")
|
||||||
|
user = qRes[0]
|
||||||
|
userExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(args[1]), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not compute user password hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
if userExists {
|
||||||
|
err = db.Model(&models.User{}).Where(&user).Update("password", fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash))).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not update user password")
|
||||||
|
}
|
||||||
|
logger.Infof("updated user %s", user.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user = models.User{
|
||||||
|
Username: username,
|
||||||
|
Domain: domain,
|
||||||
|
Active: flagUserActive,
|
||||||
|
Password: fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash)),
|
||||||
|
}
|
||||||
|
err = db.Save(&user).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Fatal("could not create user")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("created user %s", user.ID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "list users",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
qRes := make([]models.User, 0)
|
||||||
|
|
||||||
|
err = db.Find(&qRes).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not list users")
|
||||||
|
}
|
||||||
|
|
||||||
|
tData := pterm.TableData{
|
||||||
|
{"id", "username", "domain", "active", "uid", "gid", "created_at", "updated_at"},
|
||||||
|
}
|
||||||
|
for _, u := range qRes {
|
||||||
|
tData = append(tData, []string{u.ID.String(), u.Username, u.Domain, fmt.Sprintf("%v", u.Active), fmt.Sprintf("%d", u.UID), fmt.Sprintf("%d", u.GID), u.CreatedAt.String(), u.UpdatedAt.String()})
|
||||||
|
}
|
||||||
|
|
||||||
|
pterm.DefaultTable.WithHasHeader().WithData(tData).Render()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserDeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete",
|
||||||
|
Short: "delete users",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
userQuery, err := buildUserQuery(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("unable to determine user")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where(&userQuery).Delete(&models.User{}).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not delete user")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("deleted user %s if it existed", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserActivateCmd = &cobra.Command{
|
||||||
|
Use: "activate",
|
||||||
|
Short: "activates a user",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
userQuery, err := buildUserQuery(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("unable to determine user")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&models.User{}).Where(&userQuery).Update("active", true).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not activate user")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("activated user %s", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserDeactivateCmd = &cobra.Command{
|
||||||
|
Use: "deactivate",
|
||||||
|
Short: "deactivates a user",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not connect to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
userQuery, err := buildUserQuery(args[0])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("unable to determine user")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&models.User{}).Where(&userQuery).Update("active", false).Error
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("could not deactivate user")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("deactivated user %s", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitUserCmd() {
|
||||||
|
UserCmd.AddCommand(UserAddCmd)
|
||||||
|
UserCmd.AddCommand(UserListCmd)
|
||||||
|
UserCmd.AddCommand(UserDeleteCmd)
|
||||||
|
UserCmd.AddCommand(UserActivateCmd)
|
||||||
|
UserCmd.AddCommand(UserDeactivateCmd)
|
||||||
|
|
||||||
|
UserAddCmd.PersistentFlags().BoolVarP(&flagUserActive, "active", "a", true, "whether or not the created user is active")
|
||||||
|
}
|
45
pkg/cmd/utils.go
Normal file
45
pkg/cmd/utils.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
uuidRegex = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||||
|
)
|
||||||
|
|
||||||
|
func splitUser(user string) (string, string, error) {
|
||||||
|
splt := strings.Split(user, "@")
|
||||||
|
if len(splt) != 2 {
|
||||||
|
return "", "", fmt.Errorf("invalid username: %s", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splt[0]) == 0 || len(splt[1]) == 0 {
|
||||||
|
return "", "", fmt.Errorf("invalid username: %s", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return splt[0], splt[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUserQuery(arg string) (*models.User, error) {
|
||||||
|
var userQuery models.User
|
||||||
|
if uuidRegex.Match([]byte(arg)) {
|
||||||
|
id, err := uuid.Parse(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userQuery.ID = id
|
||||||
|
} else {
|
||||||
|
username, domain, err := splitUser(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userQuery = models.User{Domain: domain, Username: username}
|
||||||
|
}
|
||||||
|
return &userQuery, nil
|
||||||
|
}
|
45
pkg/config/config.go
Normal file
45
pkg/config/config.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
providerConfigs "git.maurice.fr/thomas/mailout/pkg/providers/configs"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Postgres struct {
|
||||||
|
Hostname string `yaml:"hostname"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Database string `yaml:"database"`
|
||||||
|
SSLMode string `yaml:"sslmode"`
|
||||||
|
} `yaml:"postgres"`
|
||||||
|
Providers struct {
|
||||||
|
OVH *providerConfigs.OVHConfig `yaml:"ovh"`
|
||||||
|
} `yaml:"providers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
err = yaml.Unmarshal(b, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Postgres.SSLMode == "" {
|
||||||
|
cfg.Postgres.SSLMode = "disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Postgres.Port == 0 {
|
||||||
|
cfg.Postgres.Port = 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, err
|
||||||
|
}
|
31
pkg/crypto/key.go
Normal file
31
pkg/crypto/key.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateKeyPair(bytes int) (string, string, error) {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = privateKey.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
privBlock := pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Headers: nil,
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyPEM := pem.EncodeToMemory(&privBlock)
|
||||||
|
|
||||||
|
return string(privateKeyPEM), string(base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(&privateKey.PublicKey))), nil
|
||||||
|
}
|
74
pkg/database/db.go
Normal file
74
pkg/database/db.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/config"
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/models"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
createViewRawSQL = `
|
||||||
|
/*
|
||||||
|
This view basically returns to you the most recent key for
|
||||||
|
each known domain, so that when opendkim performs a where
|
||||||
|
filter on the result only one result will be returned.
|
||||||
|
Hence avoiding to make it confused and upset
|
||||||
|
*/
|
||||||
|
CREATE OR REPLACE VIEW signing_table AS
|
||||||
|
SELECT
|
||||||
|
k1.id,
|
||||||
|
k1.domain_name,
|
||||||
|
k1.created_at
|
||||||
|
FROM
|
||||||
|
dkimkeys AS k1
|
||||||
|
INNER JOIN
|
||||||
|
dkimkeys AS k2
|
||||||
|
ON
|
||||||
|
k1.created_at = (
|
||||||
|
SELECT
|
||||||
|
max(created_at)
|
||||||
|
FROM
|
||||||
|
dkimkeys
|
||||||
|
WHERE
|
||||||
|
domain_name = k1.domain_name
|
||||||
|
AND k1.active = true
|
||||||
|
)
|
||||||
|
WHERE k1.domain_name = k2.domain_name
|
||||||
|
AND k1.created_at = k2.created_at;
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDB(cfg *config.Config) (*gorm.DB, error) {
|
||||||
|
db, err := gorm.Open(
|
||||||
|
postgres.Open(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
cfg.Postgres.Hostname,
|
||||||
|
cfg.Postgres.Port,
|
||||||
|
cfg.Postgres.User,
|
||||||
|
cfg.Postgres.Password,
|
||||||
|
cfg.Postgres.Database,
|
||||||
|
cfg.Postgres.SSLMode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
&gorm.Config{})
|
||||||
|
|
||||||
|
return db, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitMigrate(db *gorm.DB) error {
|
||||||
|
err := db.AutoMigrate(&models.User{}, &models.DKIMKey{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not auto migrate database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Exec(createViewRawSQL).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create the view: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
23
pkg/models/dkimkey.go
Normal file
23
pkg/models/dkimkey.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DKIMKey struct {
|
||||||
|
ID uuid.UUID `gorm:"primaryKey;column:id;type:uuid;default:gen_random_uuid()" yaml:"id"`
|
||||||
|
Selector string `gorm:"column:selector;type:varchar(128);not null" yaml:"selector"`
|
||||||
|
DomainName string `gorm:"column:domain_name;type:varchar(256);not null" yaml:"domain_name"`
|
||||||
|
PublicKey string `gorm:"column:public_key;type:text;not null" yaml:"public_key"`
|
||||||
|
PrivateKey string `gorm:"column:private_key;type:text;not null" yaml:"private_key"`
|
||||||
|
Record string `gorm:"column:record;type:text;not null" yaml:"record"`
|
||||||
|
Active bool `gorm:"column:active;type:boolean;not null;default:true" yaml:"active"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;autoCreateTime:nano;default:now()" yaml:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:udpated_at;type:timestamp;not null;autoUpdateTime:nano;default:now()" yaml:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *DKIMKey) TableName() string {
|
||||||
|
return "dkimkeys"
|
||||||
|
}
|
24
pkg/models/user.go
Normal file
24
pkg/models/user.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid()" yaml:"id"`
|
||||||
|
Username string `gorm:"primaryKey;column:username;type:varchar(256);not null" yaml:"username"`
|
||||||
|
Domain string `gorm:"primaryKey;column:domain;type:varchar(256);not null" yaml:"domain"`
|
||||||
|
Password string `gorm:"column:password;type:varchar(256);not null" yaml:"password"`
|
||||||
|
Home string `gorm:"column:home;type:varchar(256);not null;default:/var/lib/vmail/null" yaml:"home"`
|
||||||
|
UID int `gorm:"column:uid;type:integer;not null;default:1000" yaml:"uid"`
|
||||||
|
GID int `gorm:"column:gid;type:integer;not null;default:1000" yaml:"gid"`
|
||||||
|
Active bool `gorm:"column:active;type:boolean;not null;default:true" yaml:"active"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;autoCreateTime:nano;default:now()" yaml:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:udpated_at;type:timestamp;not null;autoUpdateTime:nano;default:now()" yaml:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
8
pkg/providers/configs/ovh.go
Normal file
8
pkg/providers/configs/ovh.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package configs
|
||||||
|
|
||||||
|
type OVHConfig struct {
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
ApplicationKey string `yaml:"application_key"`
|
||||||
|
ApplicationSecret string `yaml:"application_secret"`
|
||||||
|
ConsumerKey string `yaml:"consumer_key"`
|
||||||
|
}
|
35
pkg/providers/ovh/ovh.go
Normal file
35
pkg/providers/ovh/ovh.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package ovh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maurice.fr/thomas/mailout/pkg/config"
|
||||||
|
ovhgo "github.com/ovh/go-ovh/ovh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OVHProvider struct {
|
||||||
|
Client *ovhgo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOVHProvider(cfg *config.Config) (*OVHProvider, error) {
|
||||||
|
config := cfg.Providers.OVH
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
return nil, fmt.Errorf("no ovh configuration specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := ovhgo.NewClient(
|
||||||
|
config.Endpoint,
|
||||||
|
config.ApplicationKey,
|
||||||
|
config.ApplicationSecret,
|
||||||
|
config.ConsumerKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OVHProvider{
|
||||||
|
Client: c,
|
||||||
|
}, nil
|
||||||
|
}
|
1
pkg/providers/provider.go
Normal file
1
pkg/providers/provider.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package provider
|
29
pkg/utils/domain.go
Normal file
29
pkg/utils/domain.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetZone(domain string) (string, error) {
|
||||||
|
splt := strings.Split(domain, ".")
|
||||||
|
if len(splt) == 1 {
|
||||||
|
return "", fmt.Errorf("invalid domain %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splt) == 2 {
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return splt[len(splt)-2] + "." + splt[len(splt)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSubdomain(domain string) (string, error) {
|
||||||
|
top, err := GetZone(domain)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := strings.TrimSuffix(domain, top)
|
||||||
|
return strings.TrimSuffix(sub, "."), nil
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue