42 Commits

Author SHA1 Message Date
Aine
01b15b7ac4 proper multi-error message, try better to find SMTP server, fixes #58 2023-02-14 20:26:30 +02:00
Aine
3e0ecc1c02 make banlist consistent, fixes #57 2023-02-13 22:05:48 +02:00
Aine
19e2047a2b updated deps 2023-02-13 13:02:13 +02:00
Aine
dbe4a73174 make TLS reload thread-safe on TCP listener 2023-02-13 11:58:31 +02:00
Aine
a7d5207484 Merge branch 'ssl-live-reload' into 'main'
automatic ssl live reload

See merge request etke.cc/postmoogle!42
2023-02-12 20:43:33 +00:00
Aine
0f7af734e5 automatic ssl live reload 2023-02-12 20:43:33 +00:00
Aine
7d0d8cd2e6 log all smtp connection errors 2023-02-11 21:20:41 +02:00
Aine
6d55ee40ed fix dockerfile 2023-02-11 20:55:57 +02:00
Aine
dc82d97aaa update ci; update deps (show all smtp connection errors); migrate from make to just 2023-02-11 20:49:45 +02:00
Aine
12d2fee2d4 fix cc handling 2023-02-06 15:53:16 +02:00
Aine
ddf2460dbd fix dequeue account data issue 2023-01-27 00:02:23 +02:00
Aine
3f1fd00fb6 fix file uploads from incoming emails into threads 2023-01-24 16:34:31 +02:00
Aine
ac9c27aa32 handle multiple emails in header 'To' 2023-01-09 16:23:54 +02:00
Aine
1e9558c1fc registry dual writes 2023-01-08 14:23:56 +02:00
Aine
174930fc90 allow only text message events for commands 2023-01-08 00:58:14 +02:00
Aine
0559978fa2 log level changes 2023-01-04 11:22:50 +02:00
Aine
f54b87c1f7 resync rooms every 5 minutes 2023-01-03 20:13:30 +02:00
Aine
2ac6c64d13 make banlist consistent, fixes #54 2022-12-14 00:35:15 +02:00
Aine
fcd6110790 add trusted proxies 2022-11-27 00:30:50 +02:00
Aine
8d6c4aeafe big refactoring 2022-11-25 23:33:38 +02:00
Aine
14bad9f479 update readme 2022-11-25 16:48:49 +02:00
Aine
4a76a3269d healthchecks.io integration 2022-11-25 16:23:26 +02:00
Aine
351f0fca77 speed up email checks execution 2022-11-24 21:41:45 +02:00
Aine
363ba313e0 update readme 2022-11-23 21:33:29 +02:00
Aine
3115373118 SPF and DKIM checks 2022-11-23 21:30:13 +02:00
Aine
0701f8c9c3 reject wrong email in SMTP MAIL(), reject impersonation attempts 2022-11-23 11:51:12 +02:00
Aine
b4d6d992ac do not react on edits and redactions, add section titles in help message 2022-11-21 23:57:49 +02:00
Aine
21772d7360 mailbox activation, closes #52 2022-11-21 15:37:44 +02:00
Aine
a5edaaea78 respect nosend in thread replies, respect nohtml in !pm send and thread replies (on sending) 2022-11-21 10:50:06 +02:00
Aine
6ddb894577 allow reserved mailboxes, closes #43 2022-11-20 20:55:41 +02:00
Aine
117736dcf3 use correct list of recipients on thread reply and in 'email has been sent' messages 2022-11-20 00:58:51 +02:00
Aine
bb7cf4aa7a cleanup From, To and Cc. Send replies to all recipients (To+Cc) 2022-11-20 00:31:59 +02:00
Aine
8007f77535 Merge branch 'addmeto.cc' into 'main'
correctly handle TCP connections without forging them for banned hosts

See merge request etke.cc/postmoogle!41
2022-11-19 16:21:23 +00:00
Aine
ced98e818e correctly handle TCP connections without forging them for banned hosts 2022-11-19 18:20:57 +02:00
Aine
9d25b9455f Merge branch 'addmeto.cc' into 'main'
CC/BCC support

See merge request etke.cc/postmoogle!40
2022-11-19 16:06:29 +00:00
Aine
1bcf9bb050 set correct Message-Id, From, To, Cc, based on previous emails (and used domain) in the thread 2022-11-19 18:05:26 +02:00
Aine
128d2b595a use the same sender's domain on thread reply as in parent email 2022-11-19 17:41:38 +02:00
Aine
8aac16aca8 make thread replies CC-aware and multi-domain aware 2022-11-19 17:38:13 +02:00
Aine
5fe8603506 add nocc option 2022-11-19 17:09:24 +02:00
Aine
052fd5bb25 refactoring, created email package 2022-11-19 17:00:57 +02:00
Aine
9e532a6007 initial cc support 2022-11-19 16:41:53 +02:00
Aine
ad83eab930 force <style></style> removal in html part of incoming emails 2022-11-19 00:48:48 +02:00
267 changed files with 36145 additions and 13299 deletions

View File

@@ -6,23 +6,23 @@ lint:
stage: test stage: test
image: registry.gitlab.com/etke.cc/base/build image: registry.gitlab.com/etke.cc/base/build
script: script:
- make lint - just lint
unit: unit:
stage: test stage: test
image: registry.gitlab.com/etke.cc/base/build image: registry.gitlab.com/etke.cc/base/build
script: script:
- make test - just test
docker: docker:
stage: release stage: release
only: ['main', 'tags'] only: ['main', 'tags']
services: services:
- docker:dind - docker:dind
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/jdrouet/docker-with-buildx:stable image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/jdrouet/docker-with-buildx:latest
before_script: before_script:
- apk --no-cache add make - apk --no-cache add just
script: script:
- make login docker - just login docker
tags: tags:
- docker - docker

View File

@@ -2,7 +2,7 @@ FROM registry.gitlab.com/etke.cc/base/build AS builder
WORKDIR /postmoogle WORKDIR /postmoogle
COPY . . COPY . .
RUN make build RUN just build
FROM registry.gitlab.com/etke.cc/base/app FROM registry.gitlab.com/etke.cc/base/app

View File

@@ -1,54 +0,0 @@
### CI vars
CI_LOGIN_COMMAND = @echo "Not a CI, skip login"
CI_REGISTRY_IMAGE ?= registry.gitlab.com/etke.cc/postmoogle
CI_COMMIT_TAG ?= latest
# for main branch it must be set explicitly
ifeq ($(CI_COMMIT_TAG), main)
CI_COMMIT_TAG = latest
endif
# login command
ifdef CI_JOB_TOKEN
CI_LOGIN_COMMAND = @docker login -u gitlab-ci-token -p $(CI_JOB_TOKEN) $(CI_REGISTRY)
endif
# update go dependencies
update:
go get ./cmd
go mod tidy
go mod verify
go mod vendor
mock:
-@rm -rf mocks
@mockery --all
# run linter
lint:
golangci-lint run ./...
# run linter and fix issues if possible
lintfix:
golangci-lint run --fix ./...
# run unit tests
test:
@go test -coverprofile=cover.out ./...
@go tool cover -func=cover.out
-@rm -f cover.out
# note: make doesn't understand exit code 130 and sets it == 1
run:
@go run ./cmd || exit 0
build:
go build -v -o postmoogle ./cmd
# CI: docker login
login:
@echo "trying to login to docker registry..."
$(CI_LOGIN_COMMAND)
# docker build
docker:
docker buildx create --use
docker buildx build --platform linux/arm64/v8,linux/amd64 --push -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG} .

View File

@@ -13,6 +13,7 @@ so you can use it to send emails from your apps and scripts as well.
### Receive ### Receive
- [x] SMTP server (plaintext and SSL) - [x] SMTP server (plaintext and SSL)
- [x] live reload of SSL certs
- [x] Matrix bot - [x] Matrix bot
- [x] Configuration in room's account data - [x] Configuration in room's account data
- [x] Receive emails to matrix rooms - [x] Receive emails to matrix rooms
@@ -20,16 +21,13 @@ so you can use it to send emails from your apps and scripts as well.
- [x] Catch-all mailbox - [x] Catch-all mailbox
- [x] Map email threads to matrix threads - [x] Map email threads to matrix threads
- [x] Multi-domain support - [x] Multi-domain support
- [x] automatic banlist - [x] SMTP verification
- [x] automatic greylisting - [x] DKIM verification
- [x] SPF verification
#### deep dive - [x] MX verification
- [x] Spamlist of emails (wildcards supported)
> features in that section considered as "nice to have", but not a priority - [x] Spamlist of hosts (per server only)
- [x] Greylisting (per server only)
- [ ] DKIM verification
- [ ] SPF verification
- [ ] DMARC verification
### Send ### Send
@@ -53,6 +51,7 @@ env vars
<summary>other optional config parameters</summary> <summary>other optional config parameters</summary>
* **POSTMOOGLE_PORT** - SMTP port to listen for new emails * **POSTMOOGLE_PORT** - SMTP port to listen for new emails
* **POSTMOOGLE_PROXIES** - space separated list of IP addresses considered as trusted proxies, thus never banned
* **POSTMOOGLE_TLS_PORT** - secure SMTP port to listen for new emails. Requires valid cert and key as well * **POSTMOOGLE_TLS_PORT** - secure SMTP port to listen for new emails. Requires valid cert and key as well
* **POSTMOOGLE_TLS_CERT** - space separated list of paths to the SSL certificates (chain) of your domains, note that position in the cert list must match the position of the cert's key in the key list * **POSTMOOGLE_TLS_CERT** - space separated list of paths to the SSL certificates (chain) of your domains, note that position in the cert list must match the position of the cert's key in the key list
* **POSTMOOGLE_TLS_KEY** - space separated list of paths to the SSL certificates' private keys of your domains, note that position on the key list must match the position of cert in the cert list * **POSTMOOGLE_TLS_KEY** - space separated list of paths to the SSL certificates' private keys of your domains, note that position on the key list must match the position of cert in the cert list
@@ -60,10 +59,15 @@ env vars
* **POSTMOOGLE_DATA_SECRET** - secure key (password) to encrypt account data, must be 16, 24, or 32 bytes long * **POSTMOOGLE_DATA_SECRET** - secure key (password) to encrypt account data, must be 16, 24, or 32 bytes long
* **POSTMOOGLE_NOENCRYPTION** - disable matrix encryption (libolm) support * **POSTMOOGLE_NOENCRYPTION** - disable matrix encryption (libolm) support
* **POSTMOOGLE_STATUSMSG** - presence status message * **POSTMOOGLE_STATUSMSG** - presence status message
* **POSTMOOGLE_SENTRY_DSN** - sentry DSN * **POSTMOOGLE_MONITORING_SENTRY_DSN** - sentry DSN
* **POSTMOOGLE_MONITORING_SENTRY_RATE** - sentry sample rate, from 0 to 100 (default: 20)
* **POSTMOOGLE_MONITORING_HEALTHCHECKS_UUID** - healthchecks.io UUID
* **POSTMOOGLE_MONITORING_HEALTHCHECKS_DURATION** - heathchecks.io duration between pings in secods (default: 5)
* **POSTMOOGLE_LOGLEVEL** - log level * **POSTMOOGLE_LOGLEVEL** - log level
* **POSTMOOGLE_DB_DSN** - database connection string * **POSTMOOGLE_DB_DSN** - database connection string
* **POSTMOOGLE_DB_DIALECT** - database dialect (postgres, sqlite3) * **POSTMOOGLE_DB_DIALECT** - database dialect (postgres, sqlite3)
* **POSTMOOGLE_MAILBOXES_RESERVED** - space separated list of reserved mailboxes, [docs/mailboxes.md](docs/mailboxes.md)
* **POSTMOOGLE_MAILBOXES_ACTIVATION** - activation flow for new mailboxes, [docs/mailboxes.md](docs/mailboxes.md)
* **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes * **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes
* **POSTMOOGLE_ADMINS** - a space-separated list of admin users. See `POSTMOOGLE_USERS` for syntax examples * **POSTMOOGLE_ADMINS** - a space-separated list of admin users. See `POSTMOOGLE_USERS` for syntax examples
@@ -102,6 +106,7 @@ If you want to change them - check available options in the help message (`!pm h
* **!pm nosender** - Get or set `nosender` of the room (`true` - hide email sender; `false` - show email sender) * **!pm nosender** - Get or set `nosender` of the room (`true` - hide email sender; `false` - show email sender)
* **!pm norecipient** - Get or set `norecipient` of the room (`true` - hide recipient; `false` - show recipient) * **!pm norecipient** - Get or set `norecipient` of the room (`true` - hide recipient; `false` - show recipient)
* **!pm nocc** - Get or set `nocc` of the room (`true` - hide CC; `false` - show CC)
* **!pm nosubject** - Get or set `nosubject` of the room (`true` - hide email subject; `false` - show email subject) * **!pm nosubject** - Get or set `nosubject` of the room (`true` - hide email subject; `false` - show email subject)
* **!pm nohtml** - Get or set `nohtml` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails) * **!pm nohtml** - Get or set `nohtml` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)
* **!pm nothreads** - Get or set `nothreads` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads) * **!pm nothreads** - Get or set `nothreads` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)
@@ -110,11 +115,14 @@ If you want to change them - check available options in the help message (`!pm h
--- ---
* **!pm spamcheck:mx** - only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable) * **!pm spamcheck:mx** - only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)
* **!pm spamcheck:spf** - only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable)
* **!pm spamcheck:dkim** - only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable)
* **!pm spamcheck:smtp** - only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable) * **!pm spamcheck:smtp** - only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)
* **!pm spamlist** - Get or set `spamlist` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,noreply@*` * **!pm spamlist** - Get or set `spamlist` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,noreply@*`
--- ---
* **!pm adminroom** - Get or set admin room
* **!pm dkim** - Get DKIM signature * **!pm dkim** - Get DKIM signature
* **!pm catch-all** - Configure catch-all mailbox * **!pm catch-all** - Configure catch-all mailbox
* **!pm queue:batch** - max amount of emails to process on each queue check * **!pm queue:batch** - max amount of emails to process on each queue check

View File

@@ -41,7 +41,7 @@ func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool {
if !b.allowUsers(actorID) { if !b.allowUsers(actorID) {
return false return false
} }
cfg, err := b.getRoomSettings(targetRoomID) cfg, err := b.cfg.GetRoom(targetRoomID)
if err != nil { if err != nil {
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err) b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
return false return false
@@ -64,7 +64,7 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool {
return false return false
} }
cfg, err := b.getRoomSettings(targetRoomID) cfg, err := b.cfg.GetRoom(targetRoomID)
if err != nil { if err != nil {
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err) b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
return false return false
@@ -73,50 +73,74 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool {
return !cfg.NoSend() return !cfg.NoSend()
} }
func (b *Bot) isReserved(mailbox string) bool {
for _, reserved := range b.mbxc.Reserved {
if mailbox == reserved {
return true
}
}
return false
}
// IsGreylisted checks if host is in greylist // IsGreylisted checks if host is in greylist
func (b *Bot) IsGreylisted(addr net.Addr) bool { func (b *Bot) IsGreylisted(addr net.Addr) bool {
if b.getBotSettings().Greylist() == 0 { if b.cfg.GetBot().Greylist() == 0 {
return false return false
} }
greylist := b.getGreylist() greylist := b.cfg.GetGreylist()
greylistedAt, ok := greylist.Get(addr) greylistedAt, ok := greylist.Get(addr)
if !ok { if !ok {
b.log.Debug("greylisting %s", addr.String()) b.log.Debug("greylisting %s", addr.String())
greylist.Add(addr) greylist.Add(addr)
err := b.setGreylist(greylist) err := b.cfg.SetGreylist(greylist)
if err != nil { if err != nil {
b.log.Error("cannot update greylist with %s: %v", addr.String(), err) b.log.Error("cannot update greylist with %s: %v", addr.String(), err)
} }
return true return true
} }
duration := time.Duration(b.getBotSettings().Greylist()) * time.Minute duration := time.Duration(b.cfg.GetBot().Greylist()) * time.Minute
return greylistedAt.Add(duration).After(time.Now().UTC()) return greylistedAt.Add(duration).After(time.Now().UTC())
} }
// IsBanned checks if address is banned // IsBanned checks if address is banned
func (b *Bot) IsBanned(addr net.Addr) bool { func (b *Bot) IsBanned(addr net.Addr) bool {
return b.banlist.Has(addr) return b.cfg.GetBanlist().Has(addr)
}
// IsTrusted checks if address is a trusted (proxy)
func (b *Bot) IsTrusted(addr net.Addr) bool {
ip := utils.AddrIP(addr)
for _, proxy := range b.proxies {
if ip == proxy {
b.log.Debug("address %s is trusted", ip)
return true
}
}
return false
} }
// Ban an address // Ban an address
func (b *Bot) Ban(addr net.Addr) { func (b *Bot) Ban(addr net.Addr) {
if !b.getBotSettings().BanlistEnabled() { if !b.cfg.GetBot().BanlistEnabled() {
return return
} }
if b.IsTrusted(addr) {
b.log.Debug("banning %s", addr.String()) return
banlist := b.getBanlist() }
b.log.Debug("attempting to ban %s", addr.String())
banlist := b.cfg.GetBanlist()
banlist.Add(addr) banlist.Add(addr)
err := b.setBanlist(banlist) err := b.cfg.SetBanlist(banlist)
if err != nil { if err != nil {
b.log.Error("cannot update banlist with %s: %v", addr.String(), err) b.log.Error("cannot update banlist with %s: %v", addr.String(), err)
} }
} }
// AllowAuth check if SMTP login (email) and password are valid // AllowAuth check if SMTP login (email) and password are valid
func (b *Bot) AllowAuth(email, password string) bool { func (b *Bot) AllowAuth(email, password string) (id.RoomID, bool) {
var suffix bool var suffix bool
for _, domain := range b.domains { for _, domain := range b.domains {
if strings.HasSuffix(email, "@"+domain) { if strings.HasSuffix(email, "@"+domain) {
@@ -125,22 +149,27 @@ func (b *Bot) AllowAuth(email, password string) bool {
} }
} }
if !suffix { if !suffix {
return false return "", false
} }
roomID, ok := b.getMapping(utils.Mailbox(email)) roomID, ok := b.getMapping(utils.Mailbox(email))
if !ok { if !ok {
return false return "", false
} }
cfg, err := b.getRoomSettings(roomID) cfg, err := b.cfg.GetRoom(roomID)
if err != nil { if err != nil {
b.log.Error("failed to retrieve settings: %v", err) b.log.Error("failed to retrieve settings: %v", err)
return false return "", false
}
if cfg.NoSend() {
b.log.Warn("trying to send email from %q (%q), but it's receive-only", email, roomID)
return "", false
} }
allow, err := argon2pw.CompareHashWithPassword(cfg.Password(), password) allow, err := argon2pw.CompareHashWithPassword(cfg.Password(), password)
if err != nil { if err != nil {
b.log.Warn("Password for %s is not valid: %v", email, err) b.log.Warn("Password for %s is not valid: %v", email, err)
} }
return allow return roomID, allow
} }

54
bot/activation.go Normal file
View File

@@ -0,0 +1,54 @@
package bot
import (
"fmt"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
type activationFlow func(id.UserID, id.RoomID, string) bool
func (b *Bot) getActivationFlow() activationFlow {
switch b.mbxc.Activation {
case "none":
return b.activateNone
case "notify":
return b.activateNotify
default:
return b.activateNone
}
}
// ActivateMailbox using the configured flow
func (b *Bot) ActivateMailbox(ownerID id.UserID, roomID id.RoomID, mailbox string) bool {
flow := b.getActivationFlow()
return flow(ownerID, roomID, mailbox)
}
func (b *Bot) activateNone(ownerID id.UserID, roomID id.RoomID, mailbox string) bool {
b.log.Debug("activating mailbox %q (%q) of %q through flow 'none'", mailbox, roomID, ownerID)
b.rooms.Store(mailbox, roomID)
return true
}
func (b *Bot) activateNotify(ownerID id.UserID, roomID id.RoomID, mailbox string) bool {
b.log.Debug("activating mailbox %q (%q) of %q through flow 'notify'", mailbox, roomID, ownerID)
b.rooms.Store(mailbox, roomID)
if len(b.adminRooms) == 0 {
return true
}
msg := fmt.Sprintf("Mailbox %q has been registered by %q for the room %q", mailbox, ownerID, roomID)
for _, adminRoom := range b.adminRooms {
content := format.RenderMarkdown(msg, true, true)
_, err := b.lp.Send(adminRoom, &content)
if err != nil {
b.log.Info("cannot send mailbox activation notification to the admin room %q", adminRoom)
continue
}
break
}
return true
}

View File

@@ -12,39 +12,62 @@ import (
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/bot/queue"
"gitlab.com/etke.cc/postmoogle/utils"
) )
// Mailboxes config
type MBXConfig struct {
Reserved []string
Activation string
}
// Bot represents matrix bot // Bot represents matrix bot
type Bot struct { type Bot struct {
prefix string prefix string
mbxc MBXConfig
domains []string domains []string
allowedUsers []*regexp.Regexp allowedUsers []*regexp.Regexp
allowedAdmins []*regexp.Regexp allowedAdmins []*regexp.Regexp
adminRooms []id.RoomID
commands commandList commands commandList
banlist bglist
rooms sync.Map rooms sync.Map
proxies []string
sendmail func(string, string, string) error sendmail func(string, string, string) error
cfg *config.Manager
log *logger.Logger log *logger.Logger
lp *linkpearl.Linkpearl lp *linkpearl.Linkpearl
mu map[string]*sync.Mutex mu utils.Mutex
q *queue.Queue
handledMembershipEvents sync.Map handledMembershipEvents sync.Map
} }
// New creates a new matrix bot // New creates a new matrix bot
func New( func New(
q *queue.Queue,
lp *linkpearl.Linkpearl, lp *linkpearl.Linkpearl,
log *logger.Logger, log *logger.Logger,
cfg *config.Manager,
proxies []string,
prefix string, prefix string,
domains []string, domains []string,
admins []string, admins []string,
mbxc MBXConfig,
) (*Bot, error) { ) (*Bot, error) {
b := &Bot{ b := &Bot{
prefix: prefix, domains: domains,
domains: domains, prefix: prefix,
rooms: sync.Map{}, rooms: sync.Map{},
log: log, adminRooms: []id.RoomID{},
lp: lp, proxies: proxies,
mu: map[string]*sync.Mutex{}, mbxc: mbxc,
cfg: cfg,
log: log,
lp: lp,
mu: utils.NewMutex(),
q: q,
} }
users, err := b.initBotUsers() users, err := b.initBotUsers()
if err != nil { if err != nil {
@@ -103,7 +126,6 @@ func (b *Bot) Start(statusMsg string) error {
if err := b.syncRooms(); err != nil { if err := b.syncRooms(); err != nil {
return err return err
} }
b.syncBanlist()
b.initSync() b.initSync()
b.log.Info("Postmoogle has been started") b.log.Info("Postmoogle has been started")

View File

@@ -10,6 +10,8 @@ import (
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
) )
@@ -18,10 +20,10 @@ const (
commandStop = "stop" commandStop = "stop"
commandSend = "send" commandSend = "send"
commandDKIM = "dkim" commandDKIM = "dkim"
commandCatchAll = botOptionCatchAll commandCatchAll = config.BotCatchAll
commandUsers = botOptionUsers commandUsers = config.BotUsers
commandQueueBatch = botOptionQueueBatch commandQueueBatch = config.BotQueueBatch
commandQueueRetries = botOptionQueueRetries commandQueueRetries = config.BotQueueRetries
commandDelete = "delete" commandDelete = "delete"
commandBanlist = "banlist" commandBanlist = "banlist"
commandBanlistAdd = "banlist:add" commandBanlistAdd = "banlist:add"
@@ -67,120 +69,146 @@ func (b *Bot) initCommands() commandList {
description: "Send email", description: "Send email",
allowed: b.allowSend, allowed: b.allowSend,
}, },
{allowed: b.allowOwner}, // delimiter {allowed: b.allowOwner, description: "mailbox ownership"}, // delimiter
// options commands // options commands
{ {
key: roomOptionMailbox, key: config.RoomMailbox,
description: "Get or set mailbox of the room", description: "Get or set mailbox of the room",
sanitizer: utils.Mailbox, sanitizer: utils.Mailbox,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionDomain, key: config.RoomDomain,
description: "Get or set default domain of the room", description: "Get or set default domain of the room",
sanitizer: utils.SanitizeDomain, sanitizer: utils.SanitizeDomain,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionOwner, key: config.RoomOwner,
description: "Get or set owner of the room", description: "Get or set owner of the room",
sanitizer: func(s string) string { return s }, sanitizer: func(s string) string { return s },
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionPassword, key: config.RoomPassword,
description: "Get or set SMTP password of the room's mailbox", description: "Get or set SMTP password of the room's mailbox",
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{allowed: b.allowOwner}, // delimiter {allowed: b.allowOwner, description: "mailbox options"}, // delimiter
{ {
key: roomOptionNoSend, key: config.RoomNoSend,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - disable email sending; `false` - enable email sending)", "Get or set `%s` of the room (`true` - disable email sending; `false` - enable email sending)",
roomOptionNoSend, config.RoomNoSend,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionNoSender, key: config.RoomNoSender,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)", "Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
roomOptionNoSender, config.RoomNoSender,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionNoRecipient, key: config.RoomNoRecipient,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide recipient; `false` - show recipient)", "Get or set `%s` of the room (`true` - hide recipient; `false` - show recipient)",
roomOptionNoRecipient, config.RoomNoRecipient,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionNoSubject, key: config.RoomNoCC,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide CC; `false` - show CC)",
config.RoomNoCC,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: config.RoomNoSubject,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)", "Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
roomOptionNoSubject, config.RoomNoSubject,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionNoHTML, key: config.RoomNoHTML,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)", "Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
roomOptionNoHTML, config.RoomNoHTML,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionNoThreads, key: config.RoomNoThreads,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)", "Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
roomOptionNoThreads, config.RoomNoThreads,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionNoFiles, key: config.RoomNoFiles,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)", "Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)",
roomOptionNoFiles, config.RoomNoFiles,
), ),
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{allowed: b.allowOwner}, // delimiter {allowed: b.allowOwner, description: "mailbox antispam"}, // delimiter
{ {
key: roomOptionSpamcheckMX, key: config.RoomSpamcheckMX,
description: "only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)", description: "only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionSpamcheckSMTP, key: config.RoomSpamcheckSPF,
description: "only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: config.RoomSpamcheckDKIM,
description: "only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: config.RoomSpamcheckSMTP,
description: "only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)", description: "only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString, sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{ {
key: roomOptionSpamlist, key: config.RoomSpamlist,
description: fmt.Sprintf( description: fmt.Sprintf(
"Get or set `%s` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,spam@*`", "Get or set `%s` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,spam@*`",
roomOptionSpamlist, config.RoomSpamlist,
), ),
sanitizer: utils.SanitizeStringSlice, sanitizer: utils.SanitizeStringSlice,
allowed: b.allowOwner, allowed: b.allowOwner,
}, },
{allowed: b.allowAdmin}, // delimiter {allowed: b.allowAdmin, description: "server options"}, // delimiter
{ {
key: botOptionUsers, key: config.BotAdminRoom,
description: "Get or set admin room",
allowed: b.allowAdmin,
},
{
key: config.BotUsers,
description: "Get or set allowed users", description: "Get or set allowed users",
allowed: b.allowAdmin, allowed: b.allowAdmin,
}, },
@@ -216,9 +244,9 @@ func (b *Bot) initCommands() commandList {
description: "Delete specific mailbox", description: "Delete specific mailbox",
allowed: b.allowAdmin, allowed: b.allowAdmin,
}, },
{allowed: b.allowAdmin}, // delimiter {allowed: b.allowAdmin, description: "server antispam"}, // delimiter
{ {
key: botOptionGreylist, key: config.BotGreylist,
description: "Set automatic greylisting duration in minutes (0 - disabled)", description: "Set automatic greylisting duration in minutes (0 - disabled)",
allowed: b.allowAdmin, allowed: b.allowAdmin,
}, },
@@ -245,12 +273,36 @@ func (b *Bot) initCommands() commandList {
} }
} }
func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice []string) { func (b *Bot) handle(ctx context.Context) {
evt := eventFromContext(ctx)
err := b.lp.GetClient().MarkRead(evt.RoomID, evt.ID)
if err != nil {
b.log.Error("cannot send read receipt: %v", err)
}
content := evt.Content.AsMessage()
if content == nil {
b.Error(ctx, evt.RoomID, "cannot read message")
return
}
// ignore any type apart from text (e.g. reactions, redactions, notices, etc)
if content.MsgType != event.MsgText {
return
}
message := strings.TrimSpace(content.Body)
commandSlice := b.parseCommand(message, true)
if commandSlice == nil {
if utils.EventParent("", content) != "" {
b.SendEmailReply(ctx)
}
return
}
cmd := b.commands.get(commandSlice[0]) cmd := b.commands.get(commandSlice[0])
if cmd == nil { if cmd == nil {
return return
} }
_, err := b.lp.GetClient().UserTyping(evt.RoomID, true, 30*time.Second) _, err = b.lp.GetClient().UserTyping(evt.RoomID, true, 30*time.Second)
if err != nil { if err != nil {
b.log.Error("cannot send typing notification: %v", err) b.log.Error("cannot send typing notification: %v", err)
} }
@@ -270,13 +322,15 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
b.runSend(ctx) b.runSend(ctx)
case commandDKIM: case commandDKIM:
b.runDKIM(ctx, commandSlice) b.runDKIM(ctx, commandSlice)
case config.BotAdminRoom:
b.runAdminRoom(ctx, commandSlice)
case commandUsers: case commandUsers:
b.runUsers(ctx, commandSlice) b.runUsers(ctx, commandSlice)
case commandCatchAll: case commandCatchAll:
b.runCatchAll(ctx, commandSlice) b.runCatchAll(ctx, commandSlice)
case commandDelete: case commandDelete:
b.runDelete(ctx, commandSlice) b.runDelete(ctx, commandSlice)
case botOptionGreylist: case config.BotGreylist:
b.runGreylist(ctx, commandSlice) b.runGreylist(ctx, commandSlice)
case commandBanlist: case commandBanlist:
b.runBanlist(ctx, commandSlice) b.runBanlist(ctx, commandSlice)
@@ -319,7 +373,7 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
msg.WriteString("To get started, assign an email address to this room by sending a `") msg.WriteString("To get started, assign an email address to this room by sending a `")
msg.WriteString(b.prefix) msg.WriteString(b.prefix)
msg.WriteString(" ") msg.WriteString(" ")
msg.WriteString(roomOptionMailbox) msg.WriteString(config.RoomMailbox)
msg.WriteString(" SOME_INBOX` command.\n") msg.WriteString(" SOME_INBOX` command.\n")
msg.WriteString("You will then be able to send emails to ") msg.WriteString("You will then be able to send emails to ")
@@ -332,7 +386,7 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
func (b *Bot) sendHelp(ctx context.Context) { func (b *Bot) sendHelp(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg, serr := b.getRoomSettings(evt.RoomID) cfg, serr := b.cfg.GetRoom(evt.RoomID)
if serr != nil { if serr != nil {
b.log.Error("cannot retrieve settings: %v", serr) b.log.Error("cannot retrieve settings: %v", serr)
} }
@@ -344,7 +398,10 @@ func (b *Bot) sendHelp(ctx context.Context) {
continue continue
} }
if cmd.key == "" { if cmd.key == "" {
msg.WriteString("\n---\n") msg.WriteString("\n---\n\n")
msg.WriteString("#### ")
msg.WriteString(cmd.description)
msg.WriteString("\n")
continue continue
} }
msg.WriteString("* **`") msg.WriteString("* **`")
@@ -360,7 +417,7 @@ func (b *Bot) sendHelp(ctx context.Context) {
case true: case true:
msg.WriteString("(currently `") msg.WriteString("(currently `")
msg.WriteString(value) msg.WriteString(value)
if cmd.key == roomOptionMailbox { if cmd.key == config.RoomMailbox {
msg.WriteString(" (") msg.WriteString(" (")
msg.WriteString(utils.EmailsList(value, cfg.Domain())) msg.WriteString(utils.EmailsList(value, cfg.Domain()))
msg.WriteString(")") msg.WriteString(")")
@@ -378,6 +435,7 @@ func (b *Bot) sendHelp(ctx context.Context) {
b.SendNotice(ctx, evt.RoomID, msg.String()) b.SendNotice(ctx, evt.RoomID, msg.String())
} }
//nolint:gocognit // TODO
func (b *Bot) runSend(ctx context.Context) { func (b *Bot) runSend(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
if !b.allowSend(evt.Sender, evt.RoomID) { if !b.allowSend(evt.Sender, evt.RoomID) {
@@ -398,9 +456,8 @@ func (b *Bot) runSend(ctx context.Context) {
b.prefix)) b.prefix))
return return
} }
htmlBody := format.RenderMarkdown(body, true, true).FormattedBody
cfg, err := b.getRoomSettings(evt.RoomID) cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve room settings: %v", err) b.Error(ctx, evt.RoomID, "failed to retrieve room settings: %v", err)
return return
@@ -412,24 +469,30 @@ func (b *Bot) runSend(ctx context.Context) {
return return
} }
var htmlBody string
if !cfg.NoHTML() {
htmlBody = format.RenderMarkdown(body, true, true).FormattedBody
}
tos := strings.Split(to, ",") tos := strings.Split(to, ",")
// validate first // validate first
for _, to := range tos { for _, to := range tos {
if !utils.AddressValid(to) { if !email.AddressValid(to) {
b.Error(ctx, evt.RoomID, "email address is not valid") b.Error(ctx, evt.RoomID, "email address is not valid")
return return
} }
} }
b.lock(evt.RoomID.String()) b.mu.Lock(evt.RoomID.String())
defer b.unlock(evt.RoomID.String()) defer b.mu.Unlock(evt.RoomID.String())
domain := utils.SanitizeDomain(cfg.Domain()) domain := utils.SanitizeDomain(cfg.Domain())
from := mailbox + "@" + domain from := mailbox + "@" + domain
ID := utils.MessageID(evt.ID, domain) ID := email.MessageID(evt.ID, domain)
for _, to := range tos { for _, to := range tos {
email := utils.NewEmail(ID, "", " "+ID, subject, from, to, body, htmlBody, nil) recipients := []string{to}
data := email.Compose(b.getBotSettings().DKIMPrivateKey()) eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil)
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
if data == "" { if data == "" {
b.SendError(ctx, evt.RoomID, "email body is empty") b.SendError(ctx, evt.RoomID, "email body is empty")
return return
@@ -437,14 +500,14 @@ func (b *Bot) runSend(ctx context.Context) {
queued, err := b.Sendmail(evt.ID, from, to, data) queued, err := b.Sendmail(evt.ID, from, to, data)
if queued { if queued {
b.log.Error("cannot send email: %v", err) b.log.Error("cannot send email: %v", err)
b.saveSentMetadata(ctx, queued, evt.ID, email, &cfg) b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, cfg)
continue continue
} }
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err) b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err)
continue continue
} }
b.saveSentMetadata(ctx, false, evt.ID, email, &cfg) b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, cfg)
} }
if len(tos) > 1 { if len(tos) > 1 {
b.SendNotice(ctx, evt.RoomID, "All emails were sent.") b.SendNotice(ctx, evt.RoomID, "All emails were sent.")

View File

@@ -11,12 +11,13 @@ import (
"gitlab.com/etke.cc/go/secgen" "gitlab.com/etke.cc/go/secgen"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
) )
func (b *Bot) sendMailboxes(ctx context.Context) { func (b *Bot) sendMailboxes(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
mailboxes := map[string]roomSettings{} mailboxes := map[string]config.Room{}
slice := []string{} slice := []string{}
b.rooms.Range(func(key any, value any) bool { b.rooms.Range(func(key any, value any) bool {
if key == nil { if key == nil {
@@ -34,7 +35,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
if !ok { if !ok {
return true return true
} }
config, err := b.getRoomSettings(roomID) config, err := b.cfg.GetRoom(roomID)
if err != nil { if err != nil {
b.log.Error("cannot retrieve settings: %v", err) b.log.Error("cannot retrieve settings: %v", err)
} }
@@ -80,7 +81,7 @@ func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
roomID := v.(id.RoomID) roomID := v.(id.RoomID)
b.rooms.Delete(mailbox) b.rooms.Delete(mailbox)
err := b.setRoomSettings(roomID, roomSettings{}) err := b.cfg.SetRoom(roomID, config.Room{})
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err) b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
return return
@@ -91,7 +92,7 @@ func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
func (b *Bot) runUsers(ctx context.Context, commandSlice []string) { func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg := b.getBotSettings() cfg := b.cfg.GetBot()
if len(commandSlice) < 2 { if len(commandSlice) < 2 {
var msg strings.Builder var msg strings.Builder
users := cfg.Users() users := cfg.Users()
@@ -122,9 +123,9 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
return return
} }
cfg.Set(botOptionUsers, strings.Join(patterns, " ")) cfg.Set(config.BotUsers, strings.Join(patterns, " "))
err = b.setBotSettings(cfg) err = b.cfg.SetBot(cfg)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err) b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
} }
@@ -134,10 +135,10 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) { func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg := b.getBotSettings() cfg := b.cfg.GetBot()
if len(commandSlice) > 1 && commandSlice[1] == "reset" { if len(commandSlice) > 1 && commandSlice[1] == "reset" {
cfg.Set(botOptionDKIMPrivateKey, "") cfg.Set(config.BotDKIMPrivateKey, "")
cfg.Set(botOptionDKIMSignature, "") cfg.Set(config.BotDKIMSignature, "")
} }
signature := cfg.DKIMSignature() signature := cfg.DKIMSignature()
@@ -149,9 +150,9 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
b.Error(ctx, evt.RoomID, "cannot generate DKIM signature: %v", derr) b.Error(ctx, evt.RoomID, "cannot generate DKIM signature: %v", derr)
return return
} }
cfg.Set(botOptionDKIMSignature, signature) cfg.Set(config.BotDKIMSignature, signature)
cfg.Set(botOptionDKIMPrivateKey, private) cfg.Set(config.BotDKIMPrivateKey, private)
err := b.setBotSettings(cfg) err := b.cfg.SetBot(cfg)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err) b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return return
@@ -169,7 +170,7 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) { func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg := b.getBotSettings() cfg := b.cfg.GetBot()
if len(commandSlice) < 2 { if len(commandSlice) < 2 {
var msg strings.Builder var msg strings.Builder
msg.WriteString("Currently: `") msg.WriteString("Currently: `")
@@ -198,8 +199,8 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
return return
} }
cfg.Set(botOptionCatchAll, mailbox) cfg.Set(config.BotCatchAll, mailbox)
err := b.setBotSettings(cfg) err := b.cfg.SetBot(cfg)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err) b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return return
@@ -208,9 +209,43 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, ""))) b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")))
} }
func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.cfg.GetBot()
if len(commandSlice) < 2 {
var msg strings.Builder
msg.WriteString("Currently: `")
if cfg.AdminRoom() != "" {
msg.WriteString(cfg.AdminRoom().String())
} else {
msg.WriteString("not set")
}
msg.WriteString("`\n\n")
msg.WriteString("Usage: `")
msg.WriteString(b.prefix)
msg.WriteString(" adminroom ROOM_ID`")
msg.WriteString("where ROOM_ID is valid and existing matrix room id\n")
b.SendNotice(ctx, evt.RoomID, msg.String())
return
}
roomID := b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
cfg.Set(config.BotAdminRoom, roomID)
err := b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return
}
b.adminRooms = append([]id.RoomID{id.RoomID(roomID)}, b.adminRooms...) // make it the first room in list on the fly
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Admin Room is set to: `%s`.", roomID))
}
func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) { func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
cfg := b.getBotSettings() cfg := b.cfg.GetBot()
greylist := b.getGreylist() greylist := b.cfg.GetGreylist()
var msg strings.Builder var msg strings.Builder
size := len(greylist) size := len(greylist)
duration := cfg.Greylist() duration := cfg.Greylist()
@@ -218,7 +253,7 @@ func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
if duration == 0 { if duration == 0 {
msg.WriteString("disabled") msg.WriteString("disabled")
} else { } else {
msg.WriteString(cfg.Get(botOptionGreylist)) msg.WriteString(cfg.Get(config.BotGreylist))
msg.WriteString("min") msg.WriteString("min")
} }
msg.WriteString("`") msg.WriteString("`")
@@ -245,10 +280,10 @@ func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
b.printGreylist(ctx, evt.RoomID) b.printGreylist(ctx, evt.RoomID)
return return
} }
cfg := b.getBotSettings() cfg := b.cfg.GetBot()
value := utils.SanitizeIntString(commandSlice[1]) value := utils.SanitizeIntString(commandSlice[1])
cfg.Set(botOptionGreylist, value) cfg.Set(config.BotGreylist, value)
err := b.setBotSettings(cfg) err := b.cfg.SetBot(cfg)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err) b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
} }
@@ -257,14 +292,14 @@ func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) { func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg := b.getBotSettings() cfg := b.cfg.GetBot()
if len(commandSlice) < 2 { if len(commandSlice) < 2 {
banlist := b.getBanlist() banlist := b.cfg.GetBanlist()
var msg strings.Builder var msg strings.Builder
size := len(banlist) size := len(banlist)
if size > 0 { if size > 0 {
msg.WriteString("Currently: `") msg.WriteString("Currently: `")
msg.WriteString(cfg.Get(botOptionBanlistEnabled)) msg.WriteString(cfg.Get(config.BotBanlistEnabled))
msg.WriteString("`, total: ") msg.WriteString("`, total: ")
msg.WriteString(strconv.Itoa(size)) msg.WriteString(strconv.Itoa(size))
msg.WriteString(" hosts (`") msg.WriteString(" hosts (`")
@@ -285,12 +320,11 @@ func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
return return
} }
value := utils.SanitizeBoolString(commandSlice[1]) value := utils.SanitizeBoolString(commandSlice[1])
cfg.Set(botOptionBanlistEnabled, value) cfg.Set(config.BotBanlistEnabled, value)
err := b.setBotSettings(cfg) err := b.cfg.SetBot(cfg)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err) b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
} }
b.syncBanlist()
b.SendNotice(ctx, evt.RoomID, "banlist has been updated") b.SendNotice(ctx, evt.RoomID, "banlist has been updated")
} }
@@ -300,7 +334,11 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
b.runBanlist(ctx, commandSlice) b.runBanlist(ctx, commandSlice)
return return
} }
banlist := b.getBanlist() if !b.cfg.GetBot().BanlistEnabled() {
b.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo")
return
}
banlist := b.cfg.GetBanlist()
ips := commandSlice[1:] ips := commandSlice[1:]
for _, ip := range ips { for _, ip := range ips {
@@ -312,7 +350,7 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
banlist.Add(addr) banlist.Add(addr)
} }
err := b.setBanlist(banlist) err := b.cfg.SetBanlist(banlist)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err) b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
return return
@@ -327,7 +365,11 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
b.runBanlist(ctx, commandSlice) b.runBanlist(ctx, commandSlice)
return return
} }
banlist := b.getBanlist() if !b.cfg.GetBot().BanlistEnabled() {
b.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo")
return
}
banlist := b.cfg.GetBanlist()
ips := commandSlice[1:] ips := commandSlice[1:]
for _, ip := range ips { for _, ip := range ips {
@@ -339,7 +381,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
banlist.Remove(addr) banlist.Remove(addr)
} }
err := b.setBanlist(banlist) err := b.cfg.SetBanlist(banlist)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err) b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
return return
@@ -350,8 +392,12 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
func (b *Bot) runBanlistReset(ctx context.Context) { func (b *Bot) runBanlistReset(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
if !b.cfg.GetBot().BanlistEnabled() {
b.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo")
return
}
err := b.setBanlist(bglist{}) err := b.cfg.SetBanlist(config.List{})
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err) b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
return return

View File

@@ -3,21 +3,23 @@ package bot
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"github.com/raja/argon2pw" "github.com/raja/argon2pw"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
) )
func (b *Bot) runStop(ctx context.Context) { func (b *Bot) runStop(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg, err := b.getRoomSettings(evt.RoomID) cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err) b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
return return
} }
mailbox := cfg.Get(roomOptionMailbox) mailbox := cfg.Get(config.RoomMailbox)
if mailbox == "" { if mailbox == "" {
b.SendNotice(ctx, evt.RoomID, "that room is not configured yet") b.SendNotice(ctx, evt.RoomID, "that room is not configured yet")
return return
@@ -25,7 +27,7 @@ func (b *Bot) runStop(ctx context.Context) {
b.rooms.Delete(mailbox) b.rooms.Delete(mailbox)
err = b.setRoomSettings(evt.RoomID, roomSettings{}) err = b.cfg.SetRoom(evt.RoomID, config.Room{})
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err) b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
return return
@@ -44,7 +46,7 @@ func (b *Bot) handleOption(ctx context.Context, cmd []string) {
func (b *Bot) getOption(ctx context.Context, name string) { func (b *Bot) getOption(ctx context.Context, name string) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg, err := b.getRoomSettings(evt.RoomID) cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err) b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
return return
@@ -59,14 +61,14 @@ func (b *Bot) getOption(ctx context.Context, name string) {
return return
} }
if name == roomOptionMailbox { if name == config.RoomMailbox {
value = utils.EmailsList(value, cfg.Domain()) value = utils.EmailsList(value, cfg.Domain())
} }
msg := fmt.Sprintf("`%s` of this room is `%s`\n"+ msg := fmt.Sprintf("`%s` of this room is `%s`\n"+
"To set it to a new value, send a `%s %s VALUE` command.", "To set it to a new value, send a `%s %s VALUE` command.",
name, value, b.prefix, name) name, value, b.prefix, name)
if name == roomOptionPassword { if name == config.RoomPassword {
msg = fmt.Sprintf("There is an SMTP password already set for this room/mailbox. "+ msg = fmt.Sprintf("There is an SMTP password already set for this room/mailbox. "+
"It's stored in a secure hashed manner, so we can't tell you what the original raw password was. "+ "It's stored in a secure hashed manner, so we can't tell you what the original raw password was. "+
"To find the raw password, try to find your old message which had originally set it, "+ "To find the raw password, try to find your old message which had originally set it, "+
@@ -84,21 +86,25 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
} }
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
if name == roomOptionMailbox { // ignore request
if name == config.RoomActive {
return
}
if name == config.RoomMailbox {
existingID, ok := b.getMapping(value) existingID, ok := b.getMapping(value)
if ok && existingID != "" && existingID != evt.RoomID { if (ok && existingID != "" && existingID != evt.RoomID) || b.isReserved(value) {
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, ""))) b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, "")))
return return
} }
} }
cfg, err := b.getRoomSettings(evt.RoomID) cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err) b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
return return
} }
if name == roomOptionPassword { if name == config.RoomPassword {
value = b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case value = b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
value, err = argon2pw.GenerateSaltedHash(value) value, err = argon2pw.GenerateSaltedHash(value)
if err != nil { if err != nil {
@@ -110,23 +116,24 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
old := cfg.Get(name) old := cfg.Get(name)
cfg.Set(name, value) cfg.Set(name, value)
if name == roomOptionMailbox { if name == config.RoomMailbox {
cfg.Set(roomOptionOwner, evt.Sender.String()) cfg.Set(config.RoomOwner, evt.Sender.String())
if old != "" { if old != "" {
b.rooms.Delete(old) b.rooms.Delete(old)
} }
b.rooms.Store(value, evt.RoomID) active := b.ActivateMailbox(evt.Sender, evt.RoomID, value)
cfg.Set(config.RoomActive, strconv.FormatBool(active))
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain())) value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
} }
err = b.setRoomSettings(evt.RoomID, cfg) err = b.cfg.SetRoom(evt.RoomID, cfg)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err) b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
return return
} }
msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value) msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value)
if name == roomOptionPassword { if name == config.RoomPassword {
msg = "SMTP password has been set" msg = "SMTP password has been set"
} }
b.SendNotice(ctx, evt.RoomID, msg) b.SendNotice(ctx, evt.RoomID, msg)

92
bot/config/bot.go Normal file
View File

@@ -0,0 +1,92 @@
package config
import (
"strings"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acBotKey = "cc.etke.postmoogle.config"
// bot options keys
const (
BotAdminRoom = "adminroom"
BotUsers = "users"
BotCatchAll = "catch-all"
BotDKIMSignature = "dkim.pub"
BotDKIMPrivateKey = "dkim.pem"
BotQueueBatch = "queue:batch"
BotQueueRetries = "queue:retries"
BotBanlistEnabled = "banlist:enabled"
BotGreylist = "greylist"
)
// Bot map
type Bot map[string]string
// Get option
func (s Bot) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s Bot) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
// Users option
func (s Bot) Users() []string {
value := s.Get(BotUsers)
if value == "" {
return []string{}
}
if strings.Contains(value, " ") {
return strings.Split(value, " ")
}
return []string{value}
}
// CatchAll option
func (s Bot) CatchAll() string {
return s.Get(BotCatchAll)
}
// AdminRoom option
func (s Bot) AdminRoom() id.RoomID {
return id.RoomID(s.Get(BotAdminRoom))
}
// BanlistEnabled option
func (s Bot) BanlistEnabled() bool {
return utils.Bool(s.Get(BotBanlistEnabled))
}
// Greylist option (duration in minutes)
func (s Bot) Greylist() int {
return utils.Int(s.Get(BotGreylist))
}
// DKIMSignature (DNS TXT record)
func (s Bot) DKIMSignature() string {
return s.Get(BotDKIMSignature)
}
// DKIMPrivateKey keep it secret
func (s Bot) DKIMPrivateKey() string {
return s.Get(BotDKIMPrivateKey)
}
// QueueBatch option
func (s Bot) QueueBatch() int {
return utils.Int(s.Get(BotQueueBatch))
}
// QueueRetries option
func (s Bot) QueueRetries() int {
return utils.Int(s.Get(BotQueueRetries))
}

69
bot/config/lists.go Normal file
View File

@@ -0,0 +1,69 @@
package config
import (
"net"
"sort"
"time"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data keys
const (
acBanlistKey = "cc.etke.postmoogle.banlist"
acGreylistKey = "cc.etke.postmoogle.greylist"
)
// List config
type List map[string]string
// Slice returns slice of ban- or greylist items
func (l List) Slice() []string {
slice := make([]string, 0, len(l))
for item := range l {
slice = append(slice, item)
}
sort.Strings(slice)
return slice
}
// Has addr in ban- or greylist
func (l List) Has(addr net.Addr) bool {
_, ok := l[utils.AddrIP(addr)]
return ok
}
// Get when addr was added in ban- or greylist
func (l List) Get(addr net.Addr) (time.Time, bool) {
from := l[utils.AddrIP(addr)]
if from == "" {
return time.Time{}, false
}
t, err := time.Parse(time.RFC1123Z, from)
if err != nil {
return time.Time{}, false
}
return t, true
}
// Add an addr to ban- or greylist
func (l List) Add(addr net.Addr) {
key := utils.AddrIP(addr)
if _, ok := l[key]; ok {
return
}
l[key] = time.Now().UTC().Format(time.RFC1123Z)
}
// Remove an addr from ban- or greylist
func (l List) Remove(addr net.Addr) {
key := utils.AddrIP(addr)
if _, ok := l[key]; !ok {
return
}
delete(l, key)
}

116
bot/config/manager.go Normal file
View File

@@ -0,0 +1,116 @@
package config
import (
"gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/linkpearl"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// Manager of configs
type Manager struct {
mu utils.Mutex
log *logger.Logger
lp *linkpearl.Linkpearl
}
// New config manager
func New(lp *linkpearl.Linkpearl, log *logger.Logger) *Manager {
m := &Manager{
mu: utils.NewMutex(),
lp: lp,
log: log,
}
return m
}
// GetBot config
func (m *Manager) GetBot() Bot {
var err error
var config Bot
config, err = m.lp.GetAccountData(acBotKey)
if err != nil {
m.log.Error("cannot get bot settings: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(Bot, 0)
return config
}
return config
}
// SetBot config
func (m *Manager) SetBot(cfg Bot) error {
return utils.UnwrapError(m.lp.SetAccountData(acBotKey, cfg))
}
// GetRoom config
func (m *Manager) GetRoom(roomID id.RoomID) (Room, error) {
config, err := m.lp.GetRoomAccountData(roomID, acRoomKey)
if config == nil {
config = make(Room, 0)
}
return config, utils.UnwrapError(err)
}
// SetRoom config
func (m *Manager) SetRoom(roomID id.RoomID, cfg Room) error {
return utils.UnwrapError(m.lp.SetRoomAccountData(roomID, acRoomKey, cfg))
}
// GetBanlist config
func (m *Manager) GetBanlist() List {
if !m.GetBot().BanlistEnabled() {
return make(List, 0)
}
m.mu.Lock("banlist")
defer m.mu.Unlock("banlist")
config, err := m.lp.GetAccountData(acBanlistKey)
if err != nil {
m.log.Error("cannot get banlist: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(List, 0)
return config
}
return config
}
// SetBanlist config
func (m *Manager) SetBanlist(cfg List) error {
if !m.GetBot().BanlistEnabled() {
return nil
}
m.mu.Lock("banlist")
defer m.mu.Unlock("banlist")
if cfg == nil {
cfg = make(List, 0)
}
return utils.UnwrapError(m.lp.SetAccountData(acBanlistKey, cfg))
}
// GetGreylist config
func (m *Manager) GetGreylist() List {
config, err := m.lp.GetAccountData(acGreylistKey)
if err != nil {
m.log.Error("cannot get banlist: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(List, 0)
return config
}
return config
}
// SetGreylist config
func (m *Manager) SetGreylist(cfg List) error {
return utils.UnwrapError(m.lp.SetAccountData(acGreylistKey, cfg))
}

183
bot/config/room.go Normal file
View File

@@ -0,0 +1,183 @@
package config
import (
"strings"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acRoomKey = "cc.etke.postmoogle.settings"
type Room map[string]string
// option keys
const (
RoomActive = ".active"
RoomOwner = "owner"
RoomMailbox = "mailbox"
RoomDomain = "domain"
RoomNoSend = "nosend"
RoomNoCC = "nocc"
RoomNoSender = "nosender"
RoomNoRecipient = "norecipient"
RoomNoSubject = "nosubject"
RoomNoHTML = "nohtml"
RoomNoThreads = "nothreads"
RoomNoFiles = "nofiles"
RoomPassword = "password"
RoomSpamcheckDKIM = "spamcheck:dkim"
RoomSpamcheckSMTP = "spamcheck:smtp"
RoomSpamcheckSPF = "spamcheck:spf"
RoomSpamcheckMX = "spamcheck:mx"
RoomSpamlist = "spamlist"
)
// Get option
func (s Room) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s Room) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
func (s Room) Mailbox() string {
return s.Get(RoomMailbox)
}
func (s Room) Domain() string {
return s.Get(RoomDomain)
}
func (s Room) Owner() string {
return s.Get(RoomOwner)
}
func (s Room) Active() bool {
return utils.Bool(s.Get(RoomActive))
}
func (s Room) Password() string {
return s.Get(RoomPassword)
}
func (s Room) NoSend() bool {
return utils.Bool(s.Get(RoomNoSend))
}
func (s Room) NoCC() bool {
return utils.Bool(s.Get(RoomNoCC))
}
func (s Room) NoSender() bool {
return utils.Bool(s.Get(RoomNoSender))
}
func (s Room) NoRecipient() bool {
return utils.Bool(s.Get(RoomNoRecipient))
}
func (s Room) NoSubject() bool {
return utils.Bool(s.Get(RoomNoSubject))
}
func (s Room) NoHTML() bool {
return utils.Bool(s.Get(RoomNoHTML))
}
func (s Room) NoThreads() bool {
return utils.Bool(s.Get(RoomNoThreads))
}
func (s Room) NoFiles() bool {
return utils.Bool(s.Get(RoomNoFiles))
}
func (s Room) SpamcheckDKIM() bool {
return utils.Bool(s.Get(RoomSpamcheckDKIM))
}
func (s Room) SpamcheckSMTP() bool {
return utils.Bool(s.Get(RoomSpamcheckSMTP))
}
func (s Room) SpamcheckSPF() bool {
return utils.Bool(s.Get(RoomSpamcheckSPF))
}
func (s Room) SpamcheckMX() bool {
return utils.Bool(s.Get(RoomSpamcheckMX))
}
func (s Room) Spamlist() []string {
return utils.StringSlice(s.Get(RoomSpamlist))
}
func (s Room) MigrateSpamlistSettings() {
uniq := map[string]struct{}{}
emails := utils.StringSlice(s.Get("spamlist:emails"))
localparts := utils.StringSlice(s.Get("spamlist:localparts"))
hosts := utils.StringSlice(s.Get("spamlist:hosts"))
list := utils.StringSlice(s.Get(RoomSpamlist))
delete(s, "spamlist:emails")
delete(s, "spamlist:localparts")
delete(s, "spamlist:hosts")
for _, email := range emails {
if email == "" {
continue
}
uniq[email] = struct{}{}
}
for _, localpart := range localparts {
if localpart == "" {
continue
}
uniq[localpart+"@*"] = struct{}{}
}
for _, host := range hosts {
if host == "" {
continue
}
uniq["*@"+host] = struct{}{}
}
for _, item := range list {
if item == "" {
continue
}
uniq[item] = struct{}{}
}
spamlist := make([]string, 0, len(uniq))
for item := range uniq {
spamlist = append(spamlist, item)
}
s.Set(RoomSpamlist, strings.Join(spamlist, ","))
}
// ContentOptions converts room display settings to content options
func (s Room) ContentOptions() *email.ContentOptions {
return &email.ContentOptions{
CC: !s.NoCC(),
HTML: !s.NoHTML(),
Sender: !s.NoSender(),
Recipient: !s.NoRecipient(),
Subject: !s.NoSubject(),
Threads: !s.NoThreads(),
ToKey: "cc.etke.postmoogle.to",
CcKey: "cc.etke.postmoogle.cc",
FromKey: "cc.etke.postmoogle.from",
RcptToKey: "cc.etke.postmoogle.rcptTo",
SubjectKey: "cc.etke.postmoogle.subject",
InReplyToKey: "cc.etke.postmoogle.inReplyTo",
MessageIDKey: "cc.etke.postmoogle.messageID",
ReferencesKey: "cc.etke.postmoogle.references",
}
}

View File

@@ -1,5 +1,11 @@
package bot package bot
import (
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
)
var migrations = []string{} var migrations = []string{}
func (b *Bot) migrate() error { func (b *Bot) migrate() error {
@@ -32,32 +38,74 @@ func (b *Bot) migrate() error {
} }
func (b *Bot) syncRooms() error { func (b *Bot) syncRooms() error {
adminRooms := []id.RoomID{}
adminRoom := b.cfg.GetBot().AdminRoom()
if adminRoom != "" {
adminRooms = append(adminRooms, adminRoom)
}
resp, err := b.lp.GetClient().JoinedRooms() resp, err := b.lp.GetClient().JoinedRooms()
if err != nil { if err != nil {
return err return err
} }
for _, roomID := range resp.JoinedRooms { for _, roomID := range resp.JoinedRooms {
cfg, serr := b.getRoomSettings(roomID) b.migrateRoomSettings(roomID)
cfg, serr := b.cfg.GetRoom(roomID)
if serr != nil { if serr != nil {
continue continue
} }
b.migrateRoomSettings(roomID)
mailbox := cfg.Mailbox() mailbox := cfg.Mailbox()
if mailbox != "" { active := cfg.Active()
if mailbox != "" && active {
b.rooms.Store(mailbox, roomID) b.rooms.Store(mailbox, roomID)
} }
if cfg.Owner() != "" && b.allowAdmin(id.UserID(cfg.Owner()), "") {
adminRooms = append(adminRooms, roomID)
}
} }
b.adminRooms = adminRooms
return nil return nil
} }
func (b *Bot) syncBanlist() { func (b *Bot) migrateRoomSettings(roomID id.RoomID) {
b.lock("banlist") cfg, err := b.cfg.GetRoom(roomID)
defer b.unlock("banlist") if err != nil {
b.log.Error("cannot retrieve room settings: %v", err)
if !b.getBotSettings().BanlistEnabled() {
b.banlist = make(bglist, 0)
return return
} }
b.banlist = b.getBanlist() if _, ok := cfg[config.RoomActive]; !ok {
cfg.Set(config.RoomActive, "true")
}
if cfg["spamlist:emails"] == "" && cfg["spamlist:localparts"] == "" && cfg["spamlist:hosts"] == "" {
return
}
cfg.MigrateSpamlistSettings()
err = b.cfg.SetRoom(roomID, cfg)
if err != nil {
b.log.Error("cannot migrate room settings: %v", err)
}
}
func (b *Bot) initBotUsers() ([]string, error) {
cfg := b.cfg.GetBot()
cfgUsers := cfg.Users()
if len(cfgUsers) > 0 {
return cfgUsers, nil
}
_, homeserver, err := b.lp.GetClient().UserID.Parse()
if err != nil {
return nil, err
}
cfg.Set(config.BotUsers, "@*:"+homeserver)
return cfg.Users(), b.cfg.SetBot(cfg)
}
// SyncRooms and mailboxes
func (b *Bot) SyncRooms() {
b.syncRooms() //nolint:errcheck // nothing can be done here
} }

View File

@@ -9,12 +9,13 @@ import (
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
) )
// account data keys // account data keys
const ( const (
acQueueKey = "cc.etke.postmoogle.mailqueue"
acMessagePrefix = "cc.etke.postmoogle.message" acMessagePrefix = "cc.etke.postmoogle.message"
acLastEventPrefix = "cc.etke.postmoogle.last" acLastEventPrefix = "cc.etke.postmoogle.last"
) )
@@ -25,13 +26,16 @@ const (
eventReferencesKey = "cc.etke.postmoogle.references" eventReferencesKey = "cc.etke.postmoogle.references"
eventInReplyToKey = "cc.etke.postmoogle.inReplyTo" eventInReplyToKey = "cc.etke.postmoogle.inReplyTo"
eventSubjectKey = "cc.etke.postmoogle.subject" eventSubjectKey = "cc.etke.postmoogle.subject"
eventRcptToKey = "cc.etke.postmoogle.rcptTo"
eventFromKey = "cc.etke.postmoogle.from" eventFromKey = "cc.etke.postmoogle.from"
eventToKey = "cc.etke.postmoogle.to" eventToKey = "cc.etke.postmoogle.to"
eventCcKey = "cc.etke.postmoogle.cc"
) )
// SetSendmail sets mail sending func to the bot // SetSendmail sets mail sending func to the bot
func (b *Bot) SetSendmail(sendmail func(string, string, string) error) { func (b *Bot) SetSendmail(sendmail func(string, string, string) error) {
b.sendmail = sendmail b.sendmail = sendmail
b.q.SetSendmail(sendmail)
} }
// Sendmail tries to send email immediately, but if it gets 4xx error (greylisting), // Sendmail tries to send email immediately, but if it gets 4xx error (greylisting),
@@ -40,8 +44,8 @@ func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error)
err := b.sendmail(from, to, data) err := b.sendmail(from, to, data)
if err != nil { if err != nil {
if strings.HasPrefix(err.Error(), "4") { if strings.HasPrefix(err.Error(), "4") {
b.log.Debug("email %s (from=%s to=%s) was added to the queue: %v", eventID, from, to, err) b.log.Info("email %s (from=%s to=%s) was added to the queue: %v", eventID, from, to, err)
return true, b.enqueueEmail(eventID.String(), from, to, data) return true, b.q.Add(eventID.String(), from, to, data)
} }
return false, err return false, err
} }
@@ -51,7 +55,7 @@ func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error)
// GetDKIMprivkey returns DKIM private key // GetDKIMprivkey returns DKIM private key
func (b *Bot) GetDKIMprivkey() string { func (b *Bot) GetDKIMprivkey() string {
return b.getBotSettings().DKIMPrivateKey() return b.cfg.GetBot().DKIMPrivateKey()
} }
func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) { func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) {
@@ -72,7 +76,7 @@ func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) {
func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) { func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) {
roomID, ok := b.getMapping(mailbox) roomID, ok := b.getMapping(mailbox)
if !ok { if !ok {
catchAll := b.getBotSettings().CatchAll() catchAll := b.cfg.GetBot().CatchAll()
if catchAll == "" { if catchAll == "" {
return roomID, ok return roomID, ok
} }
@@ -83,29 +87,28 @@ func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) {
} }
// GetIFOptions returns incoming email filtering options (room settings) // GetIFOptions returns incoming email filtering options (room settings)
func (b *Bot) GetIFOptions(roomID id.RoomID) utils.IncomingFilteringOptions { func (b *Bot) GetIFOptions(roomID id.RoomID) email.IncomingFilteringOptions {
cfg, err := b.getRoomSettings(roomID) cfg, err := b.cfg.GetRoom(roomID)
if err != nil { if err != nil {
b.log.Error("cannot retrieve room settings: %v", err) b.log.Error("cannot retrieve room settings: %v", err)
return roomSettings{}
} }
return cfg return cfg
} }
// IncomingEmail sends incoming email to matrix room // IncomingEmail sends incoming email to matrix room
func (b *Bot) IncomingEmail(ctx context.Context, email *utils.Email) error { func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
roomID, ok := b.GetMapping(email.Mailbox(true)) roomID, ok := b.GetMapping(email.Mailbox(true))
if !ok { if !ok {
return errors.New("room not found") return errors.New("room not found")
} }
cfg, err := b.getRoomSettings(roomID) cfg, err := b.cfg.GetRoom(roomID)
if err != nil { if err != nil {
b.Error(ctx, roomID, "cannot get settings: %v", err) b.Error(ctx, roomID, "cannot get settings: %v", err)
} }
b.lock(roomID.String()) b.mu.Lock(roomID.String())
defer b.unlock(roomID.String()) defer b.mu.Unlock(roomID.String())
var threadID id.EventID var threadID id.EventID
if email.InReplyTo != "" || email.References != "" { if email.InReplyTo != "" || email.References != "" {
@@ -125,7 +128,6 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *utils.Email) error {
b.setThreadID(roomID, email.MessageID, threadID) b.setThreadID(roomID, email.MessageID, threadID)
b.setLastEventID(roomID, threadID, eventID) b.setLastEventID(roomID, threadID, eventID)
threadID = eventID
if !cfg.NoFiles() { if !cfg.NoFiles() {
b.sendFiles(ctx, roomID, email.Files, cfg.NoThreads(), threadID) b.sendFiles(ctx, roomID, email.Files, cfg.NoThreads(), threadID)
@@ -137,7 +139,10 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *utils.Email) error {
// SendEmailReply sends replies from matrix thread to email thread // SendEmailReply sends replies from matrix thread to email thread
func (b *Bot) SendEmailReply(ctx context.Context) { func (b *Bot) SendEmailReply(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
cfg, err := b.getRoomSettings(evt.RoomID) if !b.allowSend(evt.Sender, evt.RoomID) {
return
}
cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil { if err != nil {
b.Error(ctx, evt.RoomID, "cannot retrieve room settings: %v", err) b.Error(ctx, evt.RoomID, "cannot retrieve room settings: %v", err)
return return
@@ -147,18 +152,11 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo") b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo")
return return
} }
domain := utils.SanitizeDomain(cfg.Domain())
b.lock(evt.RoomID.String()) b.mu.Lock(evt.RoomID.String())
defer b.unlock(evt.RoomID.String()) defer b.mu.Unlock(evt.RoomID.String())
fromMailbox := mailbox + "@" + domain meta := b.getParentEmail(evt, mailbox)
meta := b.getParentEmail(evt, domain)
// when email was sent from matrix and reply was sent from matrix again
if fromMailbox != meta.From {
meta.To = meta.From
}
meta.From = fromMailbox
if meta.To == "" { if meta.To == "" {
b.Error(ctx, evt.RoomID, "cannot find parent email and continue the thread. Please, start a new email thread") b.Error(ctx, evt.RoomID, "cannot find parent email and continue the thread. Please, start a new email thread")
@@ -173,41 +171,128 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
meta.Subject = strings.SplitN(content.Body, "\n", 1)[0] meta.Subject = strings.SplitN(content.Body, "\n", 1)[0]
} }
body := content.Body body := content.Body
htmlBody := content.FormattedBody var htmlBody string
if !cfg.NoHTML() {
htmlBody = content.FormattedBody
}
meta.MessageID = utils.MessageID(evt.ID, domain) meta.MessageID = email.MessageID(evt.ID, meta.FromDomain)
meta.References = meta.References + " " + meta.MessageID meta.References = meta.References + " " + meta.MessageID
b.log.Debug("send email reply: %+v", meta) b.log.Info("sending email reply: %+v", meta)
email := utils.NewEmail(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, body, htmlBody, nil) eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil)
data := email.Compose(b.getBotSettings().DKIMPrivateKey()) data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
if data == "" { if data == "" {
b.SendError(ctx, evt.RoomID, "email body is empty") b.SendError(ctx, evt.RoomID, "email body is empty")
return return
} }
queued, err := b.Sendmail(evt.ID, meta.From, meta.To, data) var queued bool
if queued { var hasErr bool
b.log.Error("cannot send email: %v", err) recipients := meta.Recipients
b.saveSentMetadata(ctx, queued, meta.ThreadID, email, &cfg) for _, to := range recipients {
return queued, err = b.Sendmail(evt.ID, meta.From, to, data)
if queued {
b.log.Error("cannot send email: %v", err)
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg)
hasErr = true
continue
}
if err != nil {
b.Error(ctx, evt.RoomID, "cannot send email: %v", err)
hasErr = true
continue
}
} }
if err != nil { if !hasErr {
b.Error(ctx, evt.RoomID, "cannot send email: %v", err) b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg)
return
} }
b.saveSentMetadata(ctx, queued, meta.ThreadID, email, &cfg)
} }
type parentEmail struct { type parentEmail struct {
MessageID string MessageID string
ThreadID id.EventID ThreadID id.EventID
From string From string
FromDomain string
To string To string
RcptTo string
CC string
InReplyTo string InReplyTo string
References string References string
Subject string Subject string
Recipients []string
}
// fixtofrom attempts to "fix" or rather reverse the To, From and CC headers
// of parent email by using parent email as metadata source for a new email
// that will be sent from postmoogle.
// To do so, we need to reverse From and To headers, but Cc should be adjusted as well,
// thus that hacky workaround below:
func (e *parentEmail) fixtofrom(newSenderMailbox string, domains []string) string {
newSenders := make(map[string]string, len(domains))
for _, domain := range domains {
sender := newSenderMailbox + "@" + domain
newSenders[sender] = sender
}
// try to determine previous email of the room mailbox
// by matching RCPT TO, To and From fields
// why? Because of possible multi-domain setup and we won't leak information
var previousSender string
rcptToSender, ok := newSenders[e.RcptTo]
if ok {
previousSender = rcptToSender
}
toSender, ok := newSenders[e.To]
if ok {
previousSender = toSender
}
fromSender, ok := newSenders[e.From]
if ok {
previousSender = fromSender
}
// Message-Id should not leak information either
e.FromDomain = utils.SanitizeDomain(utils.Hostname(previousSender))
originalFrom := e.From
// reverse From if needed
if fromSender == "" {
e.From = previousSender
}
// reverse To if needed
if toSender != "" {
e.To = originalFrom
}
// replace previous recipient of the email which is sender now with the original From
for newSender := range newSenders {
if strings.Contains(e.CC, newSender) {
e.CC = strings.ReplaceAll(e.CC, newSender, originalFrom)
}
}
return previousSender
}
func (e *parentEmail) calculateRecipients(from string) {
recipients := map[string]struct{}{}
recipients[e.From] = struct{}{}
for _, addr := range strings.Split(email.Address(e.To), ",") {
recipients[addr] = struct{}{}
}
for _, addr := range email.AddressList(e.CC) {
recipients[addr] = struct{}{}
}
delete(recipients, from)
rcpts := make([]string, 0, len(recipients))
for rcpt := range recipients {
rcpts = append(rcpts, rcpt)
}
e.Recipients = rcpts
} }
func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) { func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
@@ -246,8 +331,8 @@ func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
return threadID, decrypted return threadID, decrypted
} }
func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail { func (b *Bot) getParentEmail(evt *event.Event, newFromMailbox string) *parentEmail {
var parent parentEmail parent := &parentEmail{}
threadID, parentEvt := b.getParentEvent(evt) threadID, parentEvt := b.getParentEvent(evt)
parent.ThreadID = threadID parent.ThreadID = threadID
if parentEvt == nil { if parentEvt == nil {
@@ -257,11 +342,15 @@ func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail {
return parent return parent
} }
parent.MessageID = utils.MessageID(parentEvt.ID, domain)
parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey) parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey)
parent.To = utils.EventField[string](&parentEvt.Content, eventToKey) parent.To = utils.EventField[string](&parentEvt.Content, eventToKey)
parent.CC = utils.EventField[string](&parentEvt.Content, eventCcKey)
parent.RcptTo = utils.EventField[string](&parentEvt.Content, eventRcptToKey)
parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey) parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey)
parent.References = utils.EventField[string](&parentEvt.Content, eventReferencesKey) parent.References = utils.EventField[string](&parentEvt.Content, eventReferencesKey)
senderEmail := parent.fixtofrom(newFromMailbox, b.domains)
parent.calculateRecipients(senderEmail)
parent.MessageID = email.MessageID(parentEvt.ID, parent.FromDomain)
if parent.InReplyTo == "" { if parent.InReplyTo == "" {
parent.InReplyTo = parent.MessageID parent.InReplyTo = parent.MessageID
} }
@@ -281,14 +370,15 @@ func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail {
// saveSentMetadata used to save metadata from !pm sent and thread reply events to a separate notice message // saveSentMetadata used to save metadata from !pm sent and thread reply events to a separate notice message
// because that metadata is needed to determine email thread relations // because that metadata is needed to determine email thread relations
func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, email *utils.Email, cfg *roomSettings) { func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, recipients []string, eml *email.Email, cfg config.Room) {
text := "Email has been sent to " + email.To addrs := strings.Join(recipients, ", ")
text := "Email has been sent to " + addrs
if queued { if queued {
text = "Email to " + email.To + " has been queued" text = "Email to " + addrs + " has been queued"
} }
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
content := email.Content(threadID, cfg.ContentOptions()) content := eml.Content(threadID, cfg.ContentOptions())
notice := format.RenderMarkdown(text, true, true) notice := format.RenderMarkdown(text, true, true)
msgContent, ok := content.Parsed.(*event.MessageEventContent) msgContent, ok := content.Parsed.(*event.MessageEventContent)
if !ok { if !ok {
@@ -305,8 +395,8 @@ func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.Eve
return return
} }
domain := utils.SanitizeDomain(cfg.Domain()) domain := utils.SanitizeDomain(cfg.Domain())
b.setThreadID(evt.RoomID, utils.MessageID(evt.ID, domain), threadID) b.setThreadID(evt.RoomID, email.MessageID(evt.ID, domain), threadID)
b.setThreadID(evt.RoomID, utils.MessageID(msgID, domain), threadID) b.setThreadID(evt.RoomID, email.MessageID(msgID, domain), threadID)
b.setLastEventID(evt.RoomID, threadID, msgID) b.setLastEventID(evt.RoomID, threadID, msgID)
} }

View File

@@ -1,30 +0,0 @@
package bot
import (
"context"
"strings"
)
func (b *Bot) handle(ctx context.Context) {
evt := eventFromContext(ctx)
err := b.lp.GetClient().MarkRead(evt.RoomID, evt.ID)
if err != nil {
b.log.Error("cannot send read receipt: %v", err)
}
content := evt.Content.AsMessage()
if content == nil {
b.Error(ctx, evt.RoomID, "cannot read message")
return
}
message := strings.TrimSpace(content.Body)
cmd := b.parseCommand(message, true)
if cmd == nil {
if content.RelatesTo != nil {
b.SendEmailReply(ctx)
}
return
}
b.handleCommand(ctx, evt, cmd)
}

View File

@@ -1,24 +0,0 @@
package bot
import (
"sync"
)
func (b *Bot) lock(key string) {
_, ok := b.mu[key]
if !ok {
b.mu[key] = &sync.Mutex{}
}
b.mu[key].Lock()
}
func (b *Bot) unlock(key string) {
_, ok := b.mu[key]
if !ok {
return
}
b.mu[key].Unlock()
delete(b.mu, key)
}

View File

@@ -1,153 +0,0 @@
package bot
import (
"strconv"
)
const (
defaultQueueBatch = 1
defaultQueueRetries = 3
)
// ProcessQueue starts queue processing
func (b *Bot) ProcessQueue() {
b.log.Debug("staring queue processing...")
cfg := b.getBotSettings()
batchSize := cfg.QueueBatch()
if batchSize == 0 {
batchSize = defaultQueueBatch
}
retries := cfg.QueueRetries()
if retries == 0 {
retries = defaultQueueRetries
}
b.popqueue(batchSize, retries)
b.log.Debug("ended queue processing")
}
// popqueue gets emails from queue and tries to send them,
// if an email was sent successfully - it will be removed from queue
func (b *Bot) popqueue(batchSize, maxTries int) {
b.lock(acQueueKey)
defer b.unlock(acQueueKey)
index, err := b.lp.GetAccountData(acQueueKey)
if err != nil {
b.log.Error("cannot get queue index: %v", err)
}
i := 0
for id, itemkey := range index {
if i > batchSize {
b.log.Debug("finished re-deliveries from queue")
return
}
if dequeue := b.processQueueItem(itemkey, maxTries); dequeue {
b.log.Debug("email %s has been delivered", id)
err = b.dequeueEmail(id)
if err != nil {
b.log.Error("cannot dequeue email %s: %v", id, err)
}
}
i++
}
}
// processQueueItem tries to process an item from queue
// returns should the item be dequeued or not
func (b *Bot) processQueueItem(itemkey string, maxRetries int) bool {
b.lock(itemkey)
defer b.unlock(itemkey)
item, err := b.lp.GetAccountData(itemkey)
if err != nil {
b.log.Error("cannot retrieve a queue item %s: %v", itemkey, err)
return false
}
b.log.Debug("processing queue item %+v", item)
attempts, err := strconv.Atoi(item["attempts"])
if err != nil {
b.log.Error("cannot parse attempts of %s: %v", itemkey, err)
return false
}
if attempts > maxRetries {
return true
}
err = b.sendmail(item["from"], item["to"], item["data"])
if err == nil {
b.log.Debug("email %s from queue was delivered")
return true
}
b.log.Debug("attempted to deliver email id=%s, retry=%s, but it's not ready yet: %v", item["id"], item["attempts"], err)
attempts++
item["attempts"] = strconv.Itoa(attempts)
err = b.lp.SetAccountData(itemkey, item)
if err != nil {
b.log.Error("cannot update attempt count on email %s: %v", itemkey, err)
}
return false
}
// enqueueEmail adds an email to the queue
func (b *Bot) enqueueEmail(id, from, to, data string) error {
itemkey := acQueueKey + "." + id
item := map[string]string{
"attempts": "0",
"data": data,
"from": from,
"to": to,
"id": id,
}
b.lock(itemkey)
defer b.unlock(itemkey)
err := b.lp.SetAccountData(itemkey, item)
if err != nil {
b.log.Error("cannot enqueue email id=%s: %v", id, err)
return err
}
b.lock(acQueueKey)
defer b.unlock(acQueueKey)
queueIndex, err := b.lp.GetAccountData(acQueueKey)
if err != nil {
b.log.Error("cannot get queue index: %v", err)
return err
}
queueIndex[id] = itemkey
err = b.lp.SetAccountData(acQueueKey, queueIndex)
if err != nil {
b.log.Error("cannot save queue index: %v", err)
return err
}
return nil
}
// dequeueEmail removes an email from the queue
func (b *Bot) dequeueEmail(id string) error {
index, err := b.lp.GetAccountData(acQueueKey)
if err != nil {
b.log.Error("cannot get queue index: %v", err)
return err
}
itemkey := index[id]
if itemkey == "" {
itemkey = acQueueKey + "." + id
}
delete(index, id)
err = b.lp.SetAccountData(acQueueKey, index)
if err != nil {
b.log.Error("cannot update queue index: %v", err)
return err
}
b.lock(itemkey)
defer b.unlock(itemkey)
return b.lp.SetAccountData(itemkey, nil)
}

79
bot/queue/manager.go Normal file
View File

@@ -0,0 +1,79 @@
package queue
import (
"gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/linkpearl"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/utils"
)
const (
acQueueKey = "cc.etke.postmoogle.mailqueue"
defaultQueueBatch = 1
defaultQueueRetries = 3
)
// Queue manager
type Queue struct {
mu utils.Mutex
lp *linkpearl.Linkpearl
cfg *config.Manager
log *logger.Logger
sendmail func(string, string, string) error
}
// New queue
func New(lp *linkpearl.Linkpearl, cfg *config.Manager, log *logger.Logger) *Queue {
return &Queue{
mu: utils.Mutex{},
lp: lp,
cfg: cfg,
log: log,
}
}
// SetSendmail func
func (q *Queue) SetSendmail(function func(string, string, string) error) {
q.sendmail = function
}
// Process queue
func (q *Queue) Process() {
q.log.Debug("staring queue processing...")
cfg := q.cfg.GetBot()
batchSize := cfg.QueueBatch()
if batchSize == 0 {
batchSize = defaultQueueBatch
}
maxRetries := cfg.QueueRetries()
if maxRetries == 0 {
maxRetries = defaultQueueRetries
}
q.mu.Lock(acQueueKey)
defer q.mu.Unlock(acQueueKey)
index, err := q.lp.GetAccountData(acQueueKey)
if err != nil {
q.log.Error("cannot get queue index: %v", err)
}
i := 0
for id, itemkey := range index {
if i > batchSize {
q.log.Debug("finished re-deliveries from queue")
return
}
if dequeue := q.try(itemkey, maxRetries); dequeue {
q.log.Info("email %q has been delivered", id)
err = q.Remove(id)
if err != nil {
q.log.Error("cannot dequeue email %q: %v", id, err)
}
}
i++
}
q.log.Debug("ended queue processing")
}

101
bot/queue/queue.go Normal file
View File

@@ -0,0 +1,101 @@
package queue
import (
"strconv"
)
// Add to queue
func (q *Queue) Add(id, from, to, data string) error {
itemkey := acQueueKey + "." + id
item := map[string]string{
"attempts": "0",
"data": data,
"from": from,
"to": to,
"id": id,
}
q.mu.Lock(itemkey)
defer q.mu.Unlock(itemkey)
err := q.lp.SetAccountData(itemkey, item)
if err != nil {
q.log.Error("cannot enqueue email id=%q: %v", id, err)
return err
}
q.mu.Lock(acQueueKey)
defer q.mu.Unlock(acQueueKey)
queueIndex, err := q.lp.GetAccountData(acQueueKey)
if err != nil {
q.log.Error("cannot get queue index: %v", err)
return err
}
queueIndex[id] = itemkey
err = q.lp.SetAccountData(acQueueKey, queueIndex)
if err != nil {
q.log.Error("cannot save queue index: %v", err)
return err
}
return nil
}
// Remove from queue
func (q *Queue) Remove(id string) error {
index, err := q.lp.GetAccountData(acQueueKey)
if err != nil {
q.log.Error("cannot get queue index: %v", err)
return err
}
itemkey := index[id]
if itemkey == "" {
itemkey = acQueueKey + "." + id
}
delete(index, id)
err = q.lp.SetAccountData(acQueueKey, index)
if err != nil {
q.log.Error("cannot update queue index: %v", err)
return err
}
q.mu.Lock(itemkey)
defer q.mu.Unlock(itemkey)
return q.lp.SetAccountData(itemkey, map[string]string{})
}
// try to send email
func (q *Queue) try(itemkey string, maxRetries int) bool {
q.mu.Lock(itemkey)
defer q.mu.Unlock(itemkey)
item, err := q.lp.GetAccountData(itemkey)
if err != nil {
q.log.Error("cannot retrieve a queue item %q: %v", itemkey, err)
return false
}
q.log.Debug("processing queue item %+v", item)
attempts, err := strconv.Atoi(item["attempts"])
if err != nil {
q.log.Error("cannot parse attempts of %q: %v", itemkey, err)
return false
}
if attempts > maxRetries {
return true
}
err = q.sendmail(item["from"], item["to"], item["data"])
if err == nil {
q.log.Info("email %q from queue was delivered")
return true
}
q.log.Info("attempted to deliver email id=%q, retry=%q, but it's not ready yet: %v", item["id"], item["attempts"], err)
attempts++
item["attempts"] = strconv.Itoa(attempts)
err = q.lp.SetAccountData(itemkey, item)
if err != nil {
q.log.Error("cannot update attempt count on email %q: %v", itemkey, err)
}
return false
}

View File

@@ -1,114 +0,0 @@
package bot
import (
"strings"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acBotSettingsKey = "cc.etke.postmoogle.config"
// bot options keys
const (
botOptionUsers = "users"
botOptionCatchAll = "catch-all"
botOptionDKIMSignature = "dkim.pub"
botOptionDKIMPrivateKey = "dkim.pem"
botOptionQueueBatch = "queue:batch"
botOptionQueueRetries = "queue:retries"
botOptionBanlistEnabled = "banlist:enabled"
botOptionGreylist = "greylist"
)
type botSettings map[string]string
// Get option
func (s botSettings) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s botSettings) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
// Users option
func (s botSettings) Users() []string {
value := s.Get(botOptionUsers)
if value == "" {
return []string{}
}
if strings.Contains(value, " ") {
return strings.Split(value, " ")
}
return []string{value}
}
// CatchAll option
func (s botSettings) CatchAll() string {
return s.Get(botOptionCatchAll)
}
// BanlistEnabled option
func (s botSettings) BanlistEnabled() bool {
return utils.Bool(s.Get(botOptionBanlistEnabled))
}
// Greylist option (duration in minutes)
func (s botSettings) Greylist() int {
return utils.Int(s.Get(botOptionGreylist))
}
// DKIMSignature (DNS TXT record)
func (s botSettings) DKIMSignature() string {
return s.Get(botOptionDKIMSignature)
}
// DKIMPrivateKey keep it secret
func (s botSettings) DKIMPrivateKey() string {
return s.Get(botOptionDKIMPrivateKey)
}
// QueueBatch option
func (s botSettings) QueueBatch() int {
return utils.Int(s.Get(botOptionQueueBatch))
}
// QueueRetries option
func (s botSettings) QueueRetries() int {
return utils.Int(s.Get(botOptionQueueRetries))
}
func (b *Bot) initBotUsers() ([]string, error) {
config := b.getBotSettings()
cfgUsers := config.Users()
if len(cfgUsers) > 0 {
return cfgUsers, nil
}
_, homeserver, err := b.lp.GetClient().UserID.Parse()
if err != nil {
return nil, err
}
config.Set(botOptionUsers, "@*:"+homeserver)
return config.Users(), b.setBotSettings(config)
}
func (b *Bot) getBotSettings() botSettings {
config, err := b.lp.GetAccountData(acBotSettingsKey)
if err != nil {
b.log.Error("cannot get bot settings: %v", utils.UnwrapError(err))
}
if config == nil {
config = map[string]string{}
}
return config
}
func (b *Bot) setBotSettings(cfg botSettings) error {
return utils.UnwrapError(b.lp.SetAccountData(acBotSettingsKey, cfg))
}

View File

@@ -1,116 +0,0 @@
package bot
import (
"net"
"sort"
"time"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data keys
const (
acBanlistKey = "cc.etke.postmoogle.banlist"
acGreylistKey = "cc.etke.postmoogle.greylist"
)
type bglist map[string]string
// Slice returns slice of ban- or greylist items
func (b bglist) Slice() []string {
slice := make([]string, 0, len(b))
for item := range b {
slice = append(slice, item)
}
sort.Strings(slice)
return slice
}
func (b bglist) getKey(addr net.Addr) string {
key := addr.String()
host, _, _ := net.SplitHostPort(key) //nolint:errcheck // either way it's ok
if host != "" {
key = host
}
return key
}
// Has addr in ban- or greylist
func (b bglist) Has(addr net.Addr) bool {
_, ok := b[b.getKey(addr)]
return ok
}
// Get when addr was added in ban- or greylist
func (b bglist) Get(addr net.Addr) (time.Time, bool) {
from := b[b.getKey(addr)]
if from == "" {
return time.Time{}, false
}
t, err := time.Parse(time.RFC1123Z, from)
if err != nil {
return time.Time{}, false
}
return t, true
}
// Add an addr to ban- or greylist
func (b bglist) Add(addr net.Addr) {
key := b.getKey(addr)
if _, ok := b[key]; ok {
return
}
b[key] = time.Now().UTC().Format(time.RFC1123Z)
}
// Remove an addr from ban- or greylist
func (b bglist) Remove(addr net.Addr) {
key := b.getKey(addr)
if _, ok := b[key]; !ok {
return
}
delete(b, key)
}
func (b *Bot) getBanlist() bglist {
config, err := b.lp.GetAccountData(acBanlistKey)
if err != nil {
b.log.Error("cannot get banlist: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(bglist, 0)
}
return config
}
func (b *Bot) setBanlist(cfg bglist) error {
b.lock("banlist")
if cfg == nil {
cfg = make(bglist, 0)
}
b.banlist = cfg
defer b.unlock("banlist")
return utils.UnwrapError(b.lp.SetAccountData(acBanlistKey, cfg))
}
func (b *Bot) getGreylist() bglist {
config, err := b.lp.GetAccountData(acGreylistKey)
if err != nil {
b.log.Error("cannot get banlist: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(bglist, 0)
}
return config
}
func (b *Bot) setGreylist(cfg bglist) error {
return utils.UnwrapError(b.lp.SetAccountData(acGreylistKey, cfg))
}

View File

@@ -1,191 +0,0 @@
package bot
import (
"strings"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acRoomSettingsKey = "cc.etke.postmoogle.settings"
// option keys
const (
roomOptionOwner = "owner"
roomOptionMailbox = "mailbox"
roomOptionDomain = "domain"
roomOptionNoSend = "nosend"
roomOptionNoSender = "nosender"
roomOptionNoRecipient = "norecipient"
roomOptionNoSubject = "nosubject"
roomOptionNoHTML = "nohtml"
roomOptionNoThreads = "nothreads"
roomOptionNoFiles = "nofiles"
roomOptionPassword = "password"
roomOptionSpamcheckSMTP = "spamcheck:smtp"
roomOptionSpamcheckMX = "spamcheck:mx"
roomOptionSpamlist = "spamlist"
)
type roomSettings map[string]string
// Get option
func (s roomSettings) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s roomSettings) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
func (s roomSettings) Mailbox() string {
return s.Get(roomOptionMailbox)
}
func (s roomSettings) Domain() string {
return s.Get(roomOptionDomain)
}
func (s roomSettings) Owner() string {
return s.Get(roomOptionOwner)
}
func (s roomSettings) Password() string {
return s.Get(roomOptionPassword)
}
func (s roomSettings) NoSend() bool {
return utils.Bool(s.Get(roomOptionNoSend))
}
func (s roomSettings) NoSender() bool {
return utils.Bool(s.Get(roomOptionNoSender))
}
func (s roomSettings) NoRecipient() bool {
return utils.Bool(s.Get(roomOptionNoRecipient))
}
func (s roomSettings) NoSubject() bool {
return utils.Bool(s.Get(roomOptionNoSubject))
}
func (s roomSettings) NoHTML() bool {
return utils.Bool(s.Get(roomOptionNoHTML))
}
func (s roomSettings) NoThreads() bool {
return utils.Bool(s.Get(roomOptionNoThreads))
}
func (s roomSettings) NoFiles() bool {
return utils.Bool(s.Get(roomOptionNoFiles))
}
func (s roomSettings) SpamcheckSMTP() bool {
return utils.Bool(s.Get(roomOptionSpamcheckSMTP))
}
func (s roomSettings) SpamcheckMX() bool {
return utils.Bool(s.Get(roomOptionSpamcheckMX))
}
func (s roomSettings) Spamlist() []string {
return utils.StringSlice(s.Get(roomOptionSpamlist))
}
func (s roomSettings) migrateSpamlistSettings() {
uniq := map[string]struct{}{}
emails := utils.StringSlice(s.Get("spamlist:emails"))
localparts := utils.StringSlice(s.Get("spamlist:localparts"))
hosts := utils.StringSlice(s.Get("spamlist:hosts"))
list := utils.StringSlice(s.Get(roomOptionSpamlist))
delete(s, "spamlist:emails")
delete(s, "spamlist:localparts")
delete(s, "spamlist:hosts")
for _, email := range emails {
if email == "" {
continue
}
uniq[email] = struct{}{}
}
for _, localpart := range localparts {
if localpart == "" {
continue
}
uniq[localpart+"@*"] = struct{}{}
}
for _, host := range hosts {
if host == "" {
continue
}
uniq["*@"+host] = struct{}{}
}
for _, item := range list {
if item == "" {
continue
}
uniq[item] = struct{}{}
}
spamlist := make([]string, 0, len(uniq))
for item := range uniq {
spamlist = append(spamlist, item)
}
s.Set(roomOptionSpamlist, strings.Join(spamlist, ","))
}
// ContentOptions converts room display settings to content options
func (s roomSettings) ContentOptions() *utils.ContentOptions {
return &utils.ContentOptions{
HTML: !s.NoHTML(),
Sender: !s.NoSender(),
Recipient: !s.NoRecipient(),
Subject: !s.NoSubject(),
Threads: !s.NoThreads(),
ToKey: eventToKey,
FromKey: eventFromKey,
SubjectKey: eventSubjectKey,
MessageIDKey: eventMessageIDkey,
InReplyToKey: eventInReplyToKey,
ReferencesKey: eventReferencesKey,
}
}
func (b *Bot) getRoomSettings(roomID id.RoomID) (roomSettings, error) {
config, err := b.lp.GetRoomAccountData(roomID, acRoomSettingsKey)
if config == nil {
config = map[string]string{}
}
return config, utils.UnwrapError(err)
}
func (b *Bot) setRoomSettings(roomID id.RoomID, cfg roomSettings) error {
return utils.UnwrapError(b.lp.SetRoomAccountData(roomID, acRoomSettingsKey, cfg))
}
func (b *Bot) migrateRoomSettings(roomID id.RoomID) {
cfg, err := b.getRoomSettings(roomID)
if err != nil {
b.log.Error("cannot retrieve room settings: %v", err)
return
}
if cfg["spamlist:emails"] == "" && cfg["spamlist:localparts"] == "" && cfg["spamlist:hosts"] == "" {
return
}
cfg.migrateSpamlistSettings()
err = b.setRoomSettings(roomID, cfg)
if err != nil {
b.log.Error("cannot migrate room settings: %v", err)
}
}

View File

@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@@ -11,17 +12,23 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/mileusna/crontab" "github.com/mileusna/crontab"
"gitlab.com/etke.cc/go/healthchecks"
"gitlab.com/etke.cc/go/logger" "gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/linkpearl" "gitlab.com/etke.cc/linkpearl"
lpcfg "gitlab.com/etke.cc/linkpearl/config" lpcfg "gitlab.com/etke.cc/linkpearl/config"
"gitlab.com/etke.cc/postmoogle/bot" "gitlab.com/etke.cc/postmoogle/bot"
mxconfig "gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/bot/queue"
"gitlab.com/etke.cc/postmoogle/config" "gitlab.com/etke.cc/postmoogle/config"
"gitlab.com/etke.cc/postmoogle/smtp" "gitlab.com/etke.cc/postmoogle/smtp"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
) )
var ( var (
q *queue.Queue
hc *healthchecks.Client
mxc *mxconfig.Manager
mxb *bot.Bot mxb *bot.Bot
cron *crontab.Crontab cron *crontab.Crontab
smtpm *smtp.Manager smtpm *smtp.Manager
@@ -43,7 +50,8 @@ func main() {
log.Debug("starting internal components...") log.Debug("starting internal components...")
initSentry(cfg) initSentry(cfg)
initBot(cfg) initHealthchecks(cfg)
initMatrix(cfg)
initSMTP(cfg) initSMTP(cfg)
initCron() initCron()
initShutdown(quit) initShutdown(quit)
@@ -61,20 +69,35 @@ func main() {
func initSentry(cfg *config.Config) { func initSentry(cfg *config.Config) {
err := sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
Dsn: cfg.Sentry.DSN, Dsn: cfg.Monitoring.SentryDSN,
AttachStacktrace: true, AttachStacktrace: true,
TracesSampleRate: float64(cfg.Monitoring.SentrySampleRate) / 100,
}) })
if err != nil { if err != nil {
log.Fatal("cannot initialize sentry: %v", err) log.Fatal("cannot initialize sentry: %v", err)
} }
} }
func initBot(cfg *config.Config) { func initHealthchecks(cfg *config.Config) {
if cfg.Monitoring.HealchecksUUID == "" {
return
}
hc = healthchecks.New(cfg.Monitoring.HealchecksUUID, func(operation string, err error) {
log.Error("healthchecks operation %q failed: %v", operation, err)
})
hc.Start(strings.NewReader("starting postmoogle"))
go hc.Auto(cfg.Monitoring.HealthechsDuration)
}
func initMatrix(cfg *config.Config) {
db, err := sql.Open(cfg.DB.Dialect, cfg.DB.DSN) db, err := sql.Open(cfg.DB.Dialect, cfg.DB.DSN)
if err != nil { if err != nil {
log.Fatal("cannot initialize SQL database: %v", err) log.Fatal("cannot initialize SQL database: %v", err)
} }
mxlog := logger.New("matrix.", cfg.LogLevel) mxlog := logger.New("matrix.", cfg.LogLevel)
cfglog := logger.New("config.", cfg.LogLevel)
qlog := logger.New("queue.", cfg.LogLevel)
lp, err := linkpearl.New(&lpcfg.Config{ lp, err := linkpearl.New(&lpcfg.Config{
Homeserver: cfg.Homeserver, Homeserver: cfg.Homeserver,
Login: cfg.Login, Login: cfg.Login,
@@ -98,7 +121,9 @@ func initBot(cfg *config.Config) {
log.Fatal("cannot initialize matrix bot: %v", err) log.Fatal("cannot initialize matrix bot: %v", err)
} }
mxb, err = bot.New(lp, mxlog, cfg.Prefix, cfg.Domains, cfg.Admins) mxc = mxconfig.New(lp, cfglog)
q = queue.New(lp, mxc, qlog)
mxb, err = bot.New(q, lp, mxlog, mxc, cfg.Proxies, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes))
if err != nil { if err != nil {
// nolint // Fatal = panic, not os.Exit() // nolint // Fatal = panic, not os.Exit()
log.Fatal("cannot start matrix bot: %v", err) log.Fatal("cannot start matrix bot: %v", err)
@@ -117,15 +142,21 @@ func initSMTP(cfg *config.Config) {
LogLevel: cfg.LogLevel, LogLevel: cfg.LogLevel,
MaxSize: cfg.MaxSize, MaxSize: cfg.MaxSize,
Bot: mxb, Bot: mxb,
Callers: []smtp.Caller{mxb, q},
}) })
} }
func initCron() { func initCron() {
cron = crontab.New() cron = crontab.New()
err := cron.AddJob("* * * * *", mxb.ProcessQueue) err := cron.AddJob("* * * * *", q.Process)
if err != nil { if err != nil {
log.Error("cannot start ProcessQueue cronjob: %v", err) log.Error("cannot start queue processing cronjob: %v", err)
}
err = cron.AddJob("*/5 * * * *", mxb.SyncRooms)
if err != nil {
log.Error("cannot start sync rooms cronjob: %v", err)
} }
} }
@@ -155,6 +186,10 @@ func shutdown() {
cron.Shutdown() cron.Shutdown()
smtpm.Stop() smtpm.Stop()
mxb.Stop() mxb.Stop()
if hc != nil {
hc.Shutdown()
hc.ExitStatus(0, strings.NewReader("shutting down postmoogle"))
}
sentry.Flush(5 * time.Second) sentry.Flush(5 * time.Second)
log.Info("Postmoogle has been stopped") log.Info("Postmoogle has been stopped")
@@ -164,8 +199,7 @@ func shutdown() {
func recovery() { func recovery() {
defer shutdown() defer shutdown()
err := recover() err := recover()
// no problem just shutdown if err != nil {
if err == nil {
sentry.CurrentHub().Recover(err) sentry.CurrentHub().Recover(err)
} }
} }

View File

@@ -1,6 +1,8 @@
package config package config
import ( import (
"time"
"gitlab.com/etke.cc/go/env" "gitlab.com/etke.cc/go/env"
) )
@@ -17,19 +19,27 @@ func New() *Config {
Prefix: env.String("prefix", defaultConfig.Prefix), Prefix: env.String("prefix", defaultConfig.Prefix),
Domains: migrateDomains("domain", "domains"), Domains: migrateDomains("domain", "domains"),
Port: env.String("port", defaultConfig.Port), Port: env.String("port", defaultConfig.Port),
Proxies: env.Slice("proxies"),
NoEncryption: env.Bool("noencryption"), NoEncryption: env.Bool("noencryption"),
DataSecret: env.String("data.secret", defaultConfig.DataSecret), DataSecret: env.String("data.secret", defaultConfig.DataSecret),
MaxSize: env.Int("maxsize", defaultConfig.MaxSize), MaxSize: env.Int("maxsize", defaultConfig.MaxSize),
StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg), StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg),
Admins: env.Slice("admins"), Admins: env.Slice("admins"),
Mailboxes: Mailboxes{
Reserved: env.Slice("mailboxes.reserved"),
Activation: env.String("mailboxes.activation", defaultConfig.Mailboxes.Activation),
},
TLS: TLS{ TLS: TLS{
Certs: env.Slice("tls.cert"), Certs: env.Slice("tls.cert"),
Keys: env.Slice("tls.key"), Keys: env.Slice("tls.key"),
Required: env.Bool("tls.required"), Required: env.Bool("tls.required"),
Port: env.String("tls.port", defaultConfig.TLS.Port), Port: env.String("tls.port", defaultConfig.TLS.Port),
}, },
Sentry: Sentry{ Monitoring: Monitoring{
DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN), SentryDSN: env.String("monitoring.sentry.dsn", env.String("sentry.dsn", "")),
SentrySampleRate: env.Int("monitoring.sentry.rate", env.Int("sentry.rate", 0)),
HealchecksUUID: env.String("monitoring.healthchecks.uuid", ""),
HealthechsDuration: time.Duration(env.Int("monitoring.healthchecks.duration", int(defaultConfig.Monitoring.HealthechsDuration))) * time.Second,
}, },
LogLevel: env.String("loglevel", defaultConfig.LogLevel), LogLevel: env.String("loglevel", defaultConfig.LogLevel),
DB: DB{ DB: DB{

View File

@@ -7,10 +7,17 @@ var defaultConfig = &Config{
Prefix: "!pm", Prefix: "!pm",
MaxSize: 1024, MaxSize: 1024,
StatusMsg: "Delivering emails", StatusMsg: "Delivering emails",
Mailboxes: Mailboxes{
Activation: "none",
},
DB: DB{ DB: DB{
DSN: "local.db", DSN: "local.db",
Dialect: "sqlite3", Dialect: "sqlite3",
}, },
Monitoring: Monitoring{
SentrySampleRate: 20,
HealthechsDuration: 5,
},
TLS: TLS{ TLS: TLS{
Port: "587", Port: "587",
}, },

View File

@@ -1,5 +1,7 @@
package config package config
import "time"
// Config of Postmoogle // Config of Postmoogle
type Config struct { type Config struct {
// Homeserver url // Homeserver url
@@ -12,6 +14,8 @@ type Config struct {
Domains []string Domains []string
// Port for SMTP // Port for SMTP
Port string Port string
// Proxies is list of trusted SMTP proxies
Proxies []string
// RoomID of the admin room // RoomID of the admin room
LogLevel string LogLevel string
// DataSecret is account data secret key (password) to encrypt all account data values // DataSecret is account data secret key (password) to encrypt all account data values
@@ -24,6 +28,8 @@ type Config struct {
MaxSize int MaxSize int
// StatusMsg of the bot // StatusMsg of the bot
StatusMsg string StatusMsg string
// Mailboxes config
Mailboxes Mailboxes
// Admins holds list of admin users (wildcards supported), e.g.: @*:example.com, @bot.*:example.com, @admin:*. Empty = no admins // Admins holds list of admin users (wildcards supported), e.g.: @*:example.com, @bot.*:example.com, @admin:*. Empty = no admins
Admins []string Admins []string
@@ -33,8 +39,8 @@ type Config struct {
// TLS config // TLS config
TLS TLS TLS TLS
// Sentry config // Monitoring config
Sentry Sentry Monitoring Monitoring
} }
// DB config // DB config
@@ -53,7 +59,16 @@ type TLS struct {
Required bool Required bool
} }
// Sentry config // Monitoring config
type Sentry struct { type Monitoring struct {
DSN string SentryDSN string
SentrySampleRate int
HealchecksUUID string
HealthechsDuration time.Duration
}
// Mailboxes config
type Mailboxes struct {
Reserved []string
Activation string
} }

25
docs/mailboxes.md Normal file
View File

@@ -0,0 +1,25 @@
# Mailboxes configuration
## `POSTMOOGLE_MAILBOXES_RESERVED`
Space separated list of reserved mailboxes, example:
```bash
export POSTMOOGLE_MAILBOXES_RESERVED=admin root postmaster
```
Nobody can create a mailbox from that list
## `POSTMOOGLE_MAILBOXES_ACTIVATION`
Type of activation flow:
### `none` (default)
If `POSTMOOGLE_MAILBOXES_ACTIVATION=none` mailbox will be just created as is, without any additional checks.
### `notify`
If `POSTMOOGLE_MAILBOXES_ACTIVATION=notify`, mailbox will be created as in `none` case **and** notification will be sent to one of the mailboxes managed by a postmoogle admin.
To make it work, a postmoogle admin (or multiple admins) should either set `!pm adminroom` or create at least one mailbox.

42
docs/tricks.md Normal file
View File

@@ -0,0 +1,42 @@
# tricks
<!-- vim-markdown-toc GitLab -->
* [Logs](#logs)
* [get most active hosts](#get-most-active-hosts)
<!-- vim-markdown-toc -->
## Logs
### get most active hosts
Even if you use postmoogle as an internal mail server and contact "outside internet" quite rarely,
you will see lots of connections to your SMTP servers from random hosts over internet that do... nothing?
They don't send any valid emails or do something meaningful, thus you can safely assume they are spammers.
To get top X (in example: top 10) hosts with biggest count of attempts to connect to your postmoogle instance, follow the steps:
1. enable debug log: `export POSTMOOGLE_LOGLEVEL=debug`
2. restart postmoogle and wait some time to get stats
3. run the following bash one-liner to show top 10 hosts by connections count:
```bash
journalctl -o cat -u postmoogle | grep "smtp.DEBUG accepted connection from " | grep -oE "[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}" | sort | uniq -ci | sort -rn | head -n 10
253 111.111.111.111
183 222.222.222.222
39 333.333.333.333
38 444.444.444.444
18 555.555.555.555
16 666.666.666.666
8 777.777.777.777
5 888.888.888.888
5 999.999.999.999
4 010.010.010.010
```
of course, IP addresses above are crafted just to visualize their place in that top, according to the number of connections done.
In reality, you will see real IP addresses here. Usually, only hosts with hundreds or thousands of connections for the last 7 days worth checking.
What's next?
Do **not** ban them right away. Check WHOIS info for each host and only after that decide if you really want to ban that host or not.

View File

@@ -1,27 +1,19 @@
package utils package email
import ( import (
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt"
"net/mail"
"strings" "strings"
"time"
"github.com/emersion/go-msgauth/dkim" "github.com/emersion/go-msgauth/dkim"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
)
// IncomingFilteringOptions for incoming mail "gitlab.com/etke.cc/postmoogle/utils"
type IncomingFilteringOptions interface { )
SpamcheckSMTP() bool
SpamcheckMX() bool
Spamlist() []string
}
// Email object // Email object
type Email struct { type Email struct {
@@ -31,50 +23,25 @@ type Email struct {
References string References string
From string From string
To string To string
RcptTo string
CC []string
Subject string Subject string
Text string Text string
HTML string HTML string
Files []*File Files []*utils.File
} }
// ContentOptions represents settings that specify how an email is to be converted to a Matrix message // New constructs Email object
type ContentOptions struct { func New(messageID, inReplyTo, references, subject, from, to, rcptto, cc, text, html string, files []*utils.File) *Email {
// On/Off
Sender bool
Recipient bool
Subject bool
HTML bool
Threads bool
// Keys
MessageIDKey string
InReplyToKey string
ReferencesKey string
SubjectKey string
FromKey string
ToKey string
}
// AddressValid checks if email address is valid
func AddressValid(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}
// MessageID generates email Message-Id from matrix event ID
func MessageID(eventID id.EventID, domain string) string {
return fmt.Sprintf("<%s@%s>", eventID, domain)
}
// NewEmail constructs Email object
func NewEmail(messageID, inReplyTo, references, subject, from, to, text, html string, files []*File) *Email {
email := &Email{ email := &Email{
Date: time.Now().UTC().Format(time.RFC1123Z), Date: dateNow(),
MessageID: messageID, MessageID: messageID,
InReplyTo: inReplyTo, InReplyTo: inReplyTo,
References: references, References: references,
From: from, From: Address(from),
To: to, To: Address(to),
CC: AddressList(cc),
RcptTo: Address(rcptto),
Subject: subject, Subject: subject,
Text: text, Text: text,
HTML: html, HTML: html,
@@ -82,11 +49,42 @@ func NewEmail(messageID, inReplyTo, references, subject, from, to, text, html st
} }
if html != "" { if html != "" {
var err error html = styleRegex.ReplaceAllString(html, "")
html, err = StripHTMLTag(html, "style") email.HTML = html
if err == nil { }
email.HTML = html
} return email
}
// FromEnvelope constructs Email object from envelope
func FromEnvelope(rcptto string, envelope *enmime.Envelope) *Email {
datetime, _ := envelope.Date() //nolint:errcheck // handled in dateNow()
date := dateNow(datetime)
var html string
if envelope.HTML != "" {
html = styleRegex.ReplaceAllString(envelope.HTML, "")
}
files := make([]*utils.File, 0, len(envelope.Attachments))
for _, attachment := range envelope.Attachments {
file := utils.NewFile(attachment.FileName, attachment.Content)
files = append(files, file)
}
email := &Email{
Date: date,
MessageID: envelope.GetHeader("Message-Id"),
InReplyTo: envelope.GetHeader("In-Reply-To"),
References: envelope.GetHeader("References"),
From: Address(envelope.GetHeader("From")),
To: Address(envelope.GetHeader("To")),
RcptTo: Address(rcptto),
CC: AddressList(envelope.GetHeader("Cc")),
Subject: envelope.GetHeader("Subject"),
Text: envelope.Text,
HTML: html,
Files: files,
} }
return email return email
@@ -95,9 +93,9 @@ func NewEmail(messageID, inReplyTo, references, subject, from, to, text, html st
// Mailbox returns postmoogle's mailbox, parsing it from FROM (if incoming=false) or TO (incoming=true) // Mailbox returns postmoogle's mailbox, parsing it from FROM (if incoming=false) or TO (incoming=true)
func (e *Email) Mailbox(incoming bool) string { func (e *Email) Mailbox(incoming bool) string {
if incoming { if incoming {
return Mailbox(e.To) return utils.Mailbox(e.RcptTo)
} }
return Mailbox(e.From) return utils.Mailbox(e.From)
} }
// Content converts the email object to a Matrix event content // Content converts the email object to a Matrix event content
@@ -110,7 +108,11 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
text.WriteString(" ➡️ ") text.WriteString(" ➡️ ")
text.WriteString(e.To) text.WriteString(e.To)
} }
if options.Sender || options.Recipient { if options.CC && len(e.CC) > 0 {
text.WriteString("\ncc: ")
text.WriteString(strings.Join(e.CC, ", "))
}
if options.Sender || options.Recipient || options.CC {
text.WriteString("\n\n") text.WriteString("\n\n")
} }
if options.Subject && threadID == "" { if options.Subject && threadID == "" {
@@ -125,7 +127,12 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
} }
parsed := format.RenderMarkdown(text.String(), true, true) parsed := format.RenderMarkdown(text.String(), true, true)
parsed.RelatesTo = RelatesTo(options.Threads, threadID) parsed.RelatesTo = utils.RelatesTo(options.Threads, threadID)
var cc string
if len(e.CC) > 0 {
cc = strings.Join(e.CC, ", ")
}
content := event.Content{ content := event.Content{
Raw: map[string]interface{}{ Raw: map[string]interface{}{
@@ -133,8 +140,10 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
options.InReplyToKey: e.InReplyTo, options.InReplyToKey: e.InReplyTo,
options.ReferencesKey: e.References, options.ReferencesKey: e.References,
options.SubjectKey: e.Subject, options.SubjectKey: e.Subject,
options.RcptToKey: e.RcptTo,
options.FromKey: e.From, options.FromKey: e.From,
options.ToKey: e.To, options.ToKey: e.To,
options.CcKey: cc,
}, },
Parsed: &parsed, Parsed: &parsed,
} }
@@ -166,16 +175,19 @@ func (e *Email) Compose(privkey string) string {
if e.References != "" { if e.References != "" {
mail = mail.Header("References", e.References) mail = mail.Header("References", e.References)
} }
if len(e.CC) > 0 {
for _, addr := range e.CC {
mail = mail.CC("", addr)
}
}
root, err := mail.Build() root, err := mail.Build()
if err != nil { if err != nil {
log.Error("cannot compose email: %v", err)
return "" return ""
} }
var data strings.Builder var data strings.Builder
err = root.Encode(&data) err = root.Encode(&data)
if err != nil { if err != nil {
log.Error("cannot encode email: %v", err)
return "" return ""
} }

31
email/options.go Normal file
View File

@@ -0,0 +1,31 @@
package email
// IncomingFilteringOptions for incoming mail
type IncomingFilteringOptions interface {
SpamcheckDKIM() bool
SpamcheckSMTP() bool
SpamcheckSPF() bool
SpamcheckMX() bool
Spamlist() []string
}
// ContentOptions represents settings that specify how an email is to be converted to a Matrix message
type ContentOptions struct {
// On/Off
CC bool
Sender bool
Recipient bool
Subject bool
HTML bool
Threads bool
// Keys
MessageIDKey string
InReplyToKey string
ReferencesKey string
SubjectKey string
FromKey string
ToKey string
CcKey string
RcptToKey string
}

66
email/utils.go Normal file
View File

@@ -0,0 +1,66 @@
package email
import (
"fmt"
"net/mail"
"regexp"
"strings"
"time"
"maunium.net/go/mautrix/id"
)
var styleRegex = regexp.MustCompile("<style((.|\n|\r)*?)<\\/style>")
// AddressValid checks if email address is valid
func AddressValid(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}
// MessageID generates email Message-Id from matrix event ID
func MessageID(eventID id.EventID, domain string) string {
return fmt.Sprintf("<%s@%s>", eventID, domain)
}
// Address gets email address from a valid email address notation (eg: "Jane Doe" <jane@example.com> -> jane@example.com)
func Address(email string) string {
addr, _ := mail.ParseAddress(email) //nolint:errcheck // if it fails here, nothing will help
if addr == nil {
list := AddressList(email)
if len(list) > 0 {
return strings.Join(list, ",")
}
return email
}
return addr.Address
}
// Address gets email address from a valid email address notation (eg: "Jane Doe" <jane@example.com>, john.doe@example.com -> jane@example.com, john.doe@example.com)
func AddressList(emailList string) []string {
if emailList == "" {
return []string{}
}
list, _ := mail.ParseAddressList(emailList) //nolint:errcheck // if it fails here, nothing will help
if len(list) == 0 {
return []string{}
}
addrs := make([]string, 0, len(list))
for _, addr := range list {
addrs = append(addrs, addr.Address)
}
return addrs
}
// dateNow returns Date in RFC1123 with numeric timezone
func dateNow(original ...time.Time) string {
now := time.Now().UTC()
if len(original) > 0 && !original[0].IsZero() {
now = original[0]
}
return now.Format(time.RFC1123Z)
}

28
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/emersion/go-msgauth v0.6.6 github.com/emersion/go-msgauth v0.6.6
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.15.0 github.com/emersion/go-smtp v0.15.0
github.com/fsnotify/fsnotify v1.6.0
github.com/gabriel-vasile/mimetype v1.4.1 github.com/gabriel-vasile/mimetype v1.4.1
github.com/getsentry/sentry-go v0.13.0 github.com/getsentry/sentry-go v0.13.0
github.com/jhillyerd/enmime v0.10.0 github.com/jhillyerd/enmime v0.10.0
@@ -16,41 +17,44 @@ require (
github.com/mileusna/crontab v1.2.0 github.com/mileusna/crontab v1.2.0
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
gitlab.com/etke.cc/go/env v1.0.0 gitlab.com/etke.cc/go/env v1.0.0
gitlab.com/etke.cc/go/healthchecks v1.0.1
gitlab.com/etke.cc/go/logger v1.1.0 gitlab.com/etke.cc/go/logger v1.1.0
gitlab.com/etke.cc/go/mxidwc v1.0.0 gitlab.com/etke.cc/go/mxidwc v1.0.0
gitlab.com/etke.cc/go/secgen v1.1.1 gitlab.com/etke.cc/go/secgen v1.1.1
gitlab.com/etke.cc/go/trysmtp v1.0.0 gitlab.com/etke.cc/go/trysmtp v1.1.3
gitlab.com/etke.cc/go/validator v1.0.4 gitlab.com/etke.cc/go/validator v1.0.6
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6 gitlab.com/etke.cc/linkpearl v0.0.0-20230213101923-10ee6beb7577
golang.org/x/net v0.2.0 maunium.net/go/mautrix v0.13.0
maunium.net/go/mautrix v0.12.3
) )
require ( require (
blitiri.com.ar/go/spf v1.5.1 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect
github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/rs/zerolog v1.28.0 // indirect github.com/rs/zerolog v1.29.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tidwall/gjson v1.14.3 // indirect github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.5.3 // indirect github.com/yuin/goldmark v1.5.4 // indirect
golang.org/x/crypto v0.3.0 // indirect golang.org/x/crypto v0.6.0 // indirect
golang.org/x/sys v0.2.0 // indirect golang.org/x/net v0.6.0 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.3.2 // indirect maunium.net/go/maulogger/v2 v2.3.2 // indirect
) )

58
go.sum
View File

@@ -1,3 +1,5 @@
blitiri.com.ar/go/spf v1.5.1 h1:CWUEasc44OrANJD8CzceRnRn1Jv0LttY68cYym2/pbE=
blitiri.com.ar/go/spf v1.5.1/go.mod h1:E71N92TfL4+Yyd5lpKuE9CAF2pd4JrUq1xQfkTxoNdk=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
@@ -16,6 +18,8 @@ github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDm
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
@@ -28,6 +32,8 @@ github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -45,8 +51,9 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
@@ -69,8 +76,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -78,8 +85,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -87,30 +94,32 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/etke.cc/go/env v1.0.0 h1:J98BwzOuELnjsVPFvz5wa79L7IoRV9CmrS41xLYXtSw= gitlab.com/etke.cc/go/env v1.0.0 h1:J98BwzOuELnjsVPFvz5wa79L7IoRV9CmrS41xLYXtSw=
gitlab.com/etke.cc/go/env v1.0.0/go.mod h1:e1l4RM5MA1sc0R1w/RBDAESWRwgo5cOG9gx8BKUn2C4= gitlab.com/etke.cc/go/env v1.0.0/go.mod h1:e1l4RM5MA1sc0R1w/RBDAESWRwgo5cOG9gx8BKUn2C4=
gitlab.com/etke.cc/go/healthchecks v1.0.1 h1:IxPB+r4KtEM6wf4K7MeQoH1XnuBITMGUqFaaRIgxeUY=
gitlab.com/etke.cc/go/healthchecks v1.0.1/go.mod h1:EzQjwSawh8tQEX43Ls0dI9mND6iWd5NHtmapdO24fMI=
gitlab.com/etke.cc/go/logger v1.1.0 h1:Yngp/DDLmJ0jJNLvLXrfan5Gi5QV+r7z6kCczTv8t4U= gitlab.com/etke.cc/go/logger v1.1.0 h1:Yngp/DDLmJ0jJNLvLXrfan5Gi5QV+r7z6kCczTv8t4U=
gitlab.com/etke.cc/go/logger v1.1.0/go.mod h1:8Vw5HFXlZQ5XeqvUs5zan+GnhrQyYtm/xe+yj8H/0zk= gitlab.com/etke.cc/go/logger v1.1.0/go.mod h1:8Vw5HFXlZQ5XeqvUs5zan+GnhrQyYtm/xe+yj8H/0zk=
gitlab.com/etke.cc/go/mxidwc v1.0.0 h1:6EAlJXvs3nU4RaMegYq6iFlyVvLw7JZYnZmNCGMYQP0= gitlab.com/etke.cc/go/mxidwc v1.0.0 h1:6EAlJXvs3nU4RaMegYq6iFlyVvLw7JZYnZmNCGMYQP0=
gitlab.com/etke.cc/go/mxidwc v1.0.0/go.mod h1:E/0kh45SAN9+ntTG0cwkAEKdaPxzvxVmnjwivm9nmz8= gitlab.com/etke.cc/go/mxidwc v1.0.0/go.mod h1:E/0kh45SAN9+ntTG0cwkAEKdaPxzvxVmnjwivm9nmz8=
gitlab.com/etke.cc/go/secgen v1.1.1 h1:RmKOki725HIhWJHzPtAc9X4YvBneczndchpMgoDkE8w= gitlab.com/etke.cc/go/secgen v1.1.1 h1:RmKOki725HIhWJHzPtAc9X4YvBneczndchpMgoDkE8w=
gitlab.com/etke.cc/go/secgen v1.1.1/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8= gitlab.com/etke.cc/go/secgen v1.1.1/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8=
gitlab.com/etke.cc/go/trysmtp v1.0.0 h1:f/7gSmzohKniVeLSLevI+ZsySYcPUGkT9cRlOTwjOr8= gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbeefDI=
gitlab.com/etke.cc/go/trysmtp v1.0.0/go.mod h1:KqRuIB2IPElEEbAxXmFyKtm7S5YiuEb4lxwWthccqyE= gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0=
gitlab.com/etke.cc/go/validator v1.0.4 h1:2HIBP12f/RZr/7KTNH5/PgPTzl1vi7Co3lmhNTWB31A= gitlab.com/etke.cc/go/validator v1.0.6 h1:w0Muxf9Pqw7xvF7NaaswE6d7r9U3nB2t2l5PnFMrecQ=
gitlab.com/etke.cc/go/validator v1.0.4/go.mod h1:3vdssRG4LwgdTr9IHz9MjGSEO+3/FO9hXPGMuSeweJ8= gitlab.com/etke.cc/go/validator v1.0.6/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts=
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6 h1:+HDT2/bx3Hug++aeDE/PaoRRcnKdYzEm6i2RlOAzPXo= gitlab.com/etke.cc/linkpearl v0.0.0-20230213101923-10ee6beb7577 h1:MXvoTJ9Qp+dWezXR2sXP7HTk9ijXAorEDAy/bxJOxi8=
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6/go.mod h1:Dgtu0qvymNjjky4Bu5WC8+iSohcb5xZ9CtkD3ezDqIA= gitlab.com/etke.cc/linkpearl v0.0.0-20230213101923-10ee6beb7577/go.mod h1:yjDzaWyCnr/jiK0ArcOZlynVp2j7nqobOL2m1egkKFI=
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -118,16 +127,17 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/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.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -136,5 +146,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.12.3 h1:pUeO1ThhtZxE6XibGCzDhRuxwDIFNugsreVr1yYq96k= maunium.net/go/mautrix v0.13.0 h1:CRdpMFc1kDSNnCZMcqahR9/pkDy/vgRbd+fHnSCl6Yg=
maunium.net/go/mautrix v0.12.3/go.mod h1:uOUjkOjm2C+nQS3mr9B5ATjqemZfnPHvjdd1kZezAwg= maunium.net/go/mautrix v0.13.0/go.mod h1:gYMQPsZ9lQpyKlVp+DGwOuc9LIcE/c8GZW2CvKHISgM=

44
justfile Normal file
View File

@@ -0,0 +1,44 @@
CI_REGISTRY_IMAGE := env_var_or_default("CI_REGISTRY_IMAGE", "registry.gitlab.com/etke.cc/postmoogle")
REGISTRY_IMAGE := env_var_or_default("REGISTRY_IMAGE", "registry.etke.cc/etke.cc/postmoogle")
CI_COMMIT_TAG := if env_var_or_default("CI_COMMIT_TAG", "main") == "main" { "latest" } else { env_var_or_default("CI_COMMIT_TAG", "latest") }
# show help by default
default:
@just --list --justfile {{ justfile() }}
# update go deps
update:
go get ./cmd
go mod tidy
go mod vendor
# run linter
lint:
golangci-lint run ./...
# automatically fix liter issues
lintfix:
golangci-lint run --fix ./...
# run unit tests
test:
@go test -coverprofile=cover.out ./...
@go tool cover -func=cover.out
-@rm -f cover.out
# run app
run:
@go run ./cmd
# build app
build:
go build -v -o postmoogle ./cmd
# docker login
login:
@docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
# docker build
docker:
docker buildx create --use
docker buildx build --platform linux/arm64/v8,linux/amd64 --push -t {{ CI_REGISTRY_IMAGE }}:{{ CI_COMMIT_TAG }} -t {{ REGISTRY_IMAGE }}:{{ CI_COMMIT_TAG }} .

100
smtp/fswatcher.go Normal file
View File

@@ -0,0 +1,100 @@
package smtp
import (
"math"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"gitlab.com/etke.cc/go/logger"
)
const fsdelay = 100 * time.Millisecond
type FSWatcher struct {
watcher *fsnotify.Watcher
files []string
log *logger.Logger
mu sync.Mutex
t map[string]*time.Timer
}
func NewFSWatcher(files []string, loglevel string) (*FSWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
for _, file := range files {
err = watcher.Add(filepath.Dir(file))
if err != nil {
return nil, err
}
}
fswatcher := &FSWatcher{
watcher: watcher,
files: files,
log: logger.New("fs.", loglevel),
t: make(map[string]*time.Timer),
}
return fswatcher, nil
}
func (w *FSWatcher) watch(handler func(e fsnotify.Event)) {
for {
select {
case err, ok := <-w.watcher.Errors:
if !ok {
return
}
w.log.Error("%v", err)
case e, ok := <-w.watcher.Events:
if !ok {
return
}
handler(e)
}
}
}
// Start watcher
func (w *FSWatcher) Start(handler func(e fsnotify.Event)) {
w.watch(func(e fsnotify.Event) {
var found bool
for _, f := range w.files {
if f == e.Name {
found = true
}
}
if !found {
return
}
w.mu.Lock()
t, ok := w.t[e.Name]
w.mu.Unlock()
if !ok {
t = time.AfterFunc(math.MaxInt64, func() {
w.log.Info("handling fs event %+v", e)
handler(e)
})
t.Stop()
w.mu.Lock()
w.t[e.Name] = t
w.mu.Unlock()
}
t.Reset(fsdelay)
})
}
// Stop watcher
func (w *FSWatcher) Stop() {
err := w.watcher.Close()
if err != nil {
w.log.Error("cannot stop fs watcher: %v", err)
}
}

View File

@@ -1,7 +1,9 @@
package smtp package smtp
import ( import (
"crypto/tls"
"net" "net"
"sync"
"gitlab.com/etke.cc/go/logger" "gitlab.com/etke.cc/go/logger"
) )
@@ -9,38 +11,73 @@ import (
// Listener that rejects connections from banned hosts // Listener that rejects connections from banned hosts
type Listener struct { type Listener struct {
log *logger.Logger log *logger.Logger
done chan struct{}
tls *tls.Config
tlsMu sync.Mutex
listener net.Listener listener net.Listener
isBanned func(net.Addr) bool isBanned func(net.Addr) bool
} }
func NewListener(actual net.Listener, isBanned func(net.Addr) bool, log *logger.Logger) *Listener { func NewListener(port string, tlsConfig *tls.Config, isBanned func(net.Addr) bool, log *logger.Logger) (*Listener, error) {
actual, err := net.Listen("tcp", ":"+port)
if err != nil {
return nil, err
}
return &Listener{ return &Listener{
log: log, log: log,
done: make(chan struct{}, 1),
tls: tlsConfig,
listener: actual, listener: actual,
isBanned: isBanned, isBanned: isBanned,
} }, nil
}
func (l *Listener) SetTLSConfig(cfg *tls.Config) {
l.tlsMu.Lock()
l.tls = cfg
l.tlsMu.Unlock()
} }
// Accept waits for and returns the next connection to the listener. // Accept waits for and returns the next connection to the listener.
func (l *Listener) Accept() (net.Conn, error) { func (l *Listener) Accept() (net.Conn, error) {
conn, err := l.listener.Accept() for {
if err != nil { conn, err := l.listener.Accept()
return conn, err if err != nil {
} select {
if l.isBanned(conn.RemoteAddr()) { case <-l.done:
conn.Close() return conn, err
l.log.Info("rejected connection from %q (already banned)", conn.RemoteAddr()) default:
// Due to go-smtp design, any error returned here will crash whole server, l.log.Warn("cannot accept connection: %v", err)
// thus we have to forge a connection continue
return &net.TCPConn{}, nil }
} }
if l.isBanned(conn.RemoteAddr()) {
conn.Close()
l.log.Info("rejected connection from %q (already banned)", conn.RemoteAddr())
continue
}
return conn, nil l.log.Info("accepted connection from %q", conn.RemoteAddr())
if l.tls != nil {
return l.acceptTLS(conn)
}
return conn, nil
}
}
func (l *Listener) acceptTLS(conn net.Conn) (net.Conn, error) {
l.tlsMu.Lock()
defer l.tlsMu.Unlock()
return tls.Server(conn, l.tls), nil
} }
// Close closes the listener. // Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors. // Any blocked Accept operations will be unblocked and return errors.
func (l *Listener) Close() error { func (l *Listener) Close() error {
close(l.done)
return l.listener.Close() return l.listener.Close()
} }

29
smtp/logger.go Normal file
View File

@@ -0,0 +1,29 @@
package smtp
import (
"strings"
)
// loggerWrapper is a wrapper around logger.Logger to implement smtp.Logger interface
type loggerWrapper struct {
log func(string, ...interface{})
}
func (l loggerWrapper) Printf(format string, v ...interface{}) {
l.log(format, v...)
}
func (l loggerWrapper) Println(v ...interface{}) {
msg := strings.Repeat("%v ", len(v))
l.log(msg, v...)
}
// loggerWriter is a wrapper around io.Writer to implement io.Writer interface
type loggerWriter struct {
log func(string)
}
func (l loggerWriter) Write(p []byte) (n int, err error) {
l.log(string(p))
return len(p), nil
}

View File

@@ -4,14 +4,15 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"net" "net"
"os" "sync"
"time" "time"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/fsnotify/fsnotify"
"gitlab.com/etke.cc/go/logger" "gitlab.com/etke.cc/go/logger"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/email"
) )
type Config struct { type Config struct {
@@ -26,31 +27,46 @@ type Config struct {
LogLevel string LogLevel string
MaxSize int MaxSize int
Bot matrixbot Bot matrixbot
Callers []Caller
}
type TLSConfig struct {
Listener *Listener
Config *tls.Config
Certs []string
Keys []string
Port string
Mu sync.Mutex
} }
type Manager struct { type Manager struct {
log *logger.Logger log *logger.Logger
bot matrixbot bot matrixbot
fsw *FSWatcher
smtp *smtp.Server smtp *smtp.Server
errs chan error errs chan error
port string port string
tlsPort string tls TLSConfig
tlsCfg *tls.Config
} }
type matrixbot interface { type matrixbot interface {
AllowAuth(string, string) bool AllowAuth(string, string) (id.RoomID, bool)
IsGreylisted(net.Addr) bool IsGreylisted(net.Addr) bool
IsBanned(net.Addr) bool IsBanned(net.Addr) bool
IsTrusted(net.Addr) bool
Ban(net.Addr) Ban(net.Addr)
GetMapping(string) (id.RoomID, bool) GetMapping(string) (id.RoomID, bool)
GetIFOptions(id.RoomID) utils.IncomingFilteringOptions GetIFOptions(id.RoomID) email.IncomingFilteringOptions
IncomingEmail(context.Context, *utils.Email) error IncomingEmail(context.Context, *email.Email) error
SetSendmail(func(string, string, string) error)
GetDKIMprivkey() string GetDKIMprivkey() string
} }
// Caller is Sendmail caller
type Caller interface {
SetSendmail(func(string, string, string) error)
}
// NewManager creates new SMTP server manager // NewManager creates new SMTP server manager
func NewManager(cfg *Config) *Manager { func NewManager(cfg *Config) *Manager {
log := logger.New("smtp.", cfg.LogLevel) log := logger.New("smtp.", cfg.LogLevel)
@@ -59,9 +75,12 @@ func NewManager(cfg *Config) *Manager {
bot: cfg.Bot, bot: cfg.Bot,
domains: cfg.Domains, domains: cfg.Domains,
} }
cfg.Bot.SetSendmail(mailsrv.SendEmail) for _, caller := range cfg.Callers {
caller.SetSendmail(mailsrv.SendEmail)
}
s := smtp.NewServer(mailsrv) s := smtp.NewServer(mailsrv)
s.ErrorLog = loggerWrapper{func(s string, i ...interface{}) { log.Error(s, i...) }}
s.ReadTimeout = 10 * time.Second s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024 s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024
@@ -72,18 +91,43 @@ func NewManager(cfg *Config) *Manager {
if len(cfg.Domains) == 1 { if len(cfg.Domains) == 1 {
s.Domain = cfg.Domains[0] s.Domain = cfg.Domains[0]
} }
if log.GetLevel() == "DEBUG" || log.GetLevel() == "TRACE" { if log.GetLevel() == "INFO" || log.GetLevel() == "DEBUG" || log.GetLevel() == "TRACE" {
s.Debug = os.Stdout s.Debug = loggerWriter{func(s string) { log.Info(s) }}
}
fsw, err := NewFSWatcher(append(cfg.TLSCerts, cfg.TLSKeys...), cfg.LogLevel)
if err != nil {
log.Error("cannot start FS watcher: %v", err)
} }
m := &Manager{ m := &Manager{
smtp: s, smtp: s,
bot: cfg.Bot, bot: cfg.Bot,
log: log, log: log,
port: cfg.Port, fsw: fsw,
tlsPort: cfg.TLSPort, port: cfg.Port,
tls: TLSConfig{
Certs: cfg.TLSCerts,
Keys: cfg.TLSKeys,
Port: cfg.TLSPort,
},
}
m.tls.Mu.Lock()
m.loadTLSConfig()
m.tls.Mu.Unlock()
if m.fsw != nil {
go m.fsw.Start(func(_ fsnotify.Event) {
m.tls.Mu.Lock()
defer m.tls.Mu.Unlock()
ok := m.loadTLSConfig()
if ok {
m.tls.Listener.SetTLSConfig(m.tls.Config)
}
})
} }
m.loadTLSConfig(cfg.TLSCerts, cfg.TLSKeys)
return m return m
} }
@@ -91,8 +135,8 @@ func NewManager(cfg *Config) *Manager {
func (m *Manager) Start() error { func (m *Manager) Start() error {
m.errs = make(chan error, 1) m.errs = make(chan error, 1)
go m.listen(m.port, nil) go m.listen(m.port, nil)
if m.tlsCfg != nil { if m.tls.Config != nil {
go m.listen(m.tlsPort, m.tlsCfg) go m.listen(m.tls.Port, m.tls.Config)
} }
return <-m.errs return <-m.errs
@@ -100,6 +144,7 @@ func (m *Manager) Start() error {
// Stop SMTP server // Stop SMTP server
func (m *Manager) Stop() { func (m *Manager) Stop() {
m.fsw.Stop()
err := m.smtp.Close() err := m.smtp.Close()
if err != nil { if err != nil {
m.log.Error("cannot stop SMTP server properly: %v", err) m.log.Error("cannot stop SMTP server properly: %v", err)
@@ -107,20 +152,16 @@ func (m *Manager) Stop() {
m.log.Info("SMTP server has been stopped") m.log.Info("SMTP server has been stopped")
} }
func (m *Manager) listen(port string, tlsCfg *tls.Config) { func (m *Manager) listen(port string, tlsConfig *tls.Config) {
var l net.Listener lwrapper, err := NewListener(port, tlsConfig, m.bot.IsBanned, m.log)
var err error
if tlsCfg != nil {
l, err = tls.Listen("tcp", ":"+port, tlsCfg)
} else {
l, err = net.Listen("tcp", ":"+port)
}
if err != nil { if err != nil {
m.log.Error("cannot start listener on %s: %v", port, err) m.log.Error("cannot start listener on %s: %v", port, err)
m.errs <- err m.errs <- err
return return
} }
lwrapper := NewListener(l, m.bot.IsBanned, m.log) if tlsConfig != nil {
m.tls.Listener = lwrapper
}
m.log.Info("Starting SMTP server on port %s", port) m.log.Info("Starting SMTP server on port %s", port)
err = m.smtp.Serve(lwrapper) err = m.smtp.Serve(lwrapper)
@@ -131,15 +172,17 @@ func (m *Manager) listen(port string, tlsCfg *tls.Config) {
} }
} }
func (m *Manager) loadTLSConfig(certs, keys []string) { // loadTLSConfig returns true if certs were loaded and false if not
if len(certs) == 0 || len(keys) == 0 { func (m *Manager) loadTLSConfig() bool {
m.log.Info("(re)loading TLS config")
if len(m.tls.Certs) == 0 || len(m.tls.Keys) == 0 {
m.log.Warn("SSL certificates are not provided") m.log.Warn("SSL certificates are not provided")
return return false
} }
certificates := make([]tls.Certificate, 0, len(certs)) certificates := make([]tls.Certificate, 0, len(m.tls.Certs))
for i, path := range certs { for i, path := range m.tls.Certs {
tlsCert, err := tls.LoadX509KeyPair(path, keys[i]) tlsCert, err := tls.LoadX509KeyPair(path, m.tls.Keys[i])
if err != nil { if err != nil {
m.log.Error("cannot load SSL certificate: %v", err) m.log.Error("cannot load SSL certificate: %v", err)
continue continue
@@ -147,9 +190,10 @@ func (m *Manager) loadTLSConfig(certs, keys []string) {
certificates = append(certificates, tlsCert) certificates = append(certificates, tlsCert)
} }
if len(certificates) == 0 { if len(certificates) == 0 {
return return false
} }
m.tlsCfg = &tls.Config{Certificates: certificates} m.tls.Config = &tls.Config{Certificates: certificates}
m.smtp.TLSConfig = m.tlsCfg m.smtp.TLSConfig = m.tls.Config
return true
} }

View File

@@ -10,7 +10,7 @@ import (
"gitlab.com/etke.cc/go/logger" "gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/go/trysmtp" "gitlab.com/etke.cc/go/trysmtp"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/email"
) )
var ( var (
@@ -41,25 +41,29 @@ func (m *mailServer) Login(state *smtp.ConnectionState, username, password strin
return nil, ErrBanned return nil, ErrBanned
} }
if !utils.AddressValid(username) { if !email.AddressValid(username) {
m.log.Debug("address %s is invalid", username) m.log.Debug("address %s is invalid", username)
m.bot.Ban(state.RemoteAddr) m.bot.Ban(state.RemoteAddr)
return nil, ErrBanned return nil, ErrBanned
} }
if !m.bot.AllowAuth(username, password) { roomID, allow := m.bot.AllowAuth(username, password)
if !allow {
m.log.Debug("username=%s or password=<redacted> is invalid", username) m.log.Debug("username=%s or password=<redacted> is invalid", username)
m.bot.Ban(state.RemoteAddr) m.bot.Ban(state.RemoteAddr)
return nil, ErrBanned return nil, ErrBanned
} }
return &outgoingSession{ return &outgoingSession{
ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()), ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()),
sendmail: m.SendEmail, sendmail: m.SendEmail,
privkey: m.bot.GetDKIMprivkey(), privkey: m.bot.GetDKIMprivkey(),
from: username, from: username,
log: m.log, log: m.log,
domains: m.domains, domains: m.domains,
getRoomID: m.bot.GetMapping,
fromRoom: roomID,
tos: []string{},
}, nil }, nil
} }
@@ -77,9 +81,11 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session,
receiveEmail: m.ReceiveEmail, receiveEmail: m.ReceiveEmail,
ban: m.bot.Ban, ban: m.bot.Ban,
greylisted: m.bot.IsGreylisted, greylisted: m.bot.IsGreylisted,
trusted: m.bot.IsTrusted,
log: m.log, log: m.log,
domains: m.domains, domains: m.domains,
addr: state.RemoteAddr, addr: state.RemoteAddr,
tos: []string{},
}, nil }, nil
} }
@@ -87,10 +93,13 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session,
func (m *mailServer) SendEmail(from, to, data string) error { func (m *mailServer) SendEmail(from, to, data string) error {
m.log.Debug("Sending email from %s to %s", from, to) m.log.Debug("Sending email from %s to %s", from, to)
conn, err := trysmtp.Connect(from, to) conn, err := trysmtp.Connect(from, to)
if err != nil { if conn == nil {
m.log.Error("cannot connect to SMTP server of %s: %v", to, err) m.log.Error("cannot connect to SMTP server of %s: %v", to, err)
return err return err
} }
if err != nil {
m.log.Warn("connection to the SMTP server of %s returned the following non-fatal error(-s): %v", err)
}
defer conn.Close() defer conn.Close()
var w io.WriteCloser var w io.WriteCloser
@@ -112,6 +121,6 @@ func (m *mailServer) SendEmail(from, to, data string) error {
} }
// ReceiveEmail - incoming mail into matrix room // ReceiveEmail - incoming mail into matrix room
func (m *mailServer) ReceiveEmail(ctx context.Context, email *utils.Email) error { func (m *mailServer) ReceiveEmail(ctx context.Context, eml *email.Email) error {
return m.bot.IncomingEmail(ctx, email) return m.bot.IncomingEmail(ctx, eml)
} }

View File

@@ -1,11 +1,14 @@
package smtp package smtp
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"io" "io"
"net" "net"
"strconv"
"github.com/emersion/go-msgauth/dkim"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
@@ -13,6 +16,7 @@ import (
"gitlab.com/etke.cc/go/validator" "gitlab.com/etke.cc/go/validator"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
) )
@@ -20,21 +24,23 @@ import (
type incomingSession struct { type incomingSession struct {
log *logger.Logger log *logger.Logger
getRoomID func(string) (id.RoomID, bool) getRoomID func(string) (id.RoomID, bool)
getFilters func(id.RoomID) utils.IncomingFilteringOptions getFilters func(id.RoomID) email.IncomingFilteringOptions
receiveEmail func(context.Context, *utils.Email) error receiveEmail func(context.Context, *email.Email) error
greylisted func(net.Addr) bool greylisted func(net.Addr) bool
trusted func(net.Addr) bool
ban func(net.Addr) ban func(net.Addr)
domains []string domains []string
roomID id.RoomID
ctx context.Context ctx context.Context
addr net.Addr addr net.Addr
to string tos []string
from string from string
} }
func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error { func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from) sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
if !utils.AddressValid(from) { if !email.AddressValid(from) {
s.log.Debug("address %s is invalid", from) s.log.Debug("address %s is invalid", from)
s.ban(s.addr) s.ban(s.addr)
return ErrBanned return ErrBanned
@@ -46,10 +52,11 @@ func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error {
func (s *incomingSession) Rcpt(to string) error { func (s *incomingSession) Rcpt(to string) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to) sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
s.to = to s.tos = append(s.tos, to)
hostname := utils.Hostname(to)
var domainok bool var domainok bool
for _, domain := range s.domains { for _, domain := range s.domains {
if utils.Hostname(to) == domain { if hostname == domain {
domainok = true domainok = true
break break
} }
@@ -59,77 +66,143 @@ func (s *incomingSession) Rcpt(to string) error {
return ErrNoUser return ErrNoUser
} }
roomID, ok := s.getRoomID(utils.Mailbox(to)) var ok bool
s.roomID, ok = s.getRoomID(utils.Mailbox(to))
if !ok { if !ok {
s.log.Debug("mapping for %s not found", to) s.log.Debug("mapping for %s not found", to)
return ErrNoUser return ErrNoUser
} }
validations := s.getFilters(roomID)
if !validateEmail(s.from, s.to, s.log, validations) {
s.ban(s.addr)
return ErrBanned
}
s.log.Debug("mail to %s", to) s.log.Debug("mail to %s", to)
return nil return nil
} }
// getAddr gets real address of incoming email serder,
// including special case of trusted proxy
func (s *incomingSession) getAddr(envelope *enmime.Envelope) net.Addr {
if !s.trusted(s.addr) {
return s.addr
}
addrHeader := envelope.GetHeader("X-Real-Addr")
if addrHeader == "" {
return s.addr
}
host, portString, _ := net.SplitHostPort(addrHeader) //nolint:errcheck
if host == "" {
return s.addr
}
var port int
port, _ = strconv.Atoi(portString) //nolint:errcheck
realAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port}
s.log.Info("real address: %s", realAddr.String())
return realAddr
}
func (s *incomingSession) Data(r io.Reader) error { func (s *incomingSession) Data(r io.Reader) error {
if s.greylisted(s.addr) { data, err := io.ReadAll(r)
if err != nil {
s.log.Error("cannot read DATA: %v", err)
return err
}
reader := bytes.NewReader(data)
parser := enmime.NewParser()
envelope, err := parser.ReadEnvelope(reader)
if err != nil {
return err
}
addr := s.getAddr(envelope)
reader.Seek(0, io.SeekStart) //nolint:errcheck
validations := s.getFilters(s.roomID)
if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) {
s.ban(addr)
return ErrBanned
}
if s.greylisted(addr) {
return &smtp.SMTPError{ return &smtp.SMTPError{
Code: 451, Code: 451,
EnhancedCode: smtp.EnhancedCode{4, 5, 1}, EnhancedCode: smtp.EnhancedCode{4, 5, 1},
Message: "You have been greylisted, try again a bit later.", Message: "You have been greylisted, try again a bit later.",
} }
} }
parser := enmime.NewParser() if validations.SpamcheckDKIM() {
eml, err := parser.ReadEnvelope(r) results, verr := dkim.Verify(reader)
if err != nil { if verr != nil {
return err s.log.Error("cannot verify DKIM: %v", verr)
return verr
}
for _, result := range results {
if result.Err != nil {
s.log.Info("DKIM verification of %q failed: %v", result.Domain, result.Err)
return result.Err
}
}
} }
files := parseAttachments(eml.Attachments, s.log) eml := email.FromEnvelope(s.tos[0], envelope)
for _, to := range s.tos {
email := utils.NewEmail( eml.RcptTo = to
eml.GetHeader("Message-Id"), err := s.receiveEmail(s.ctx, eml)
eml.GetHeader("In-Reply-To"), if err != nil {
eml.GetHeader("References"), return err
eml.GetHeader("Subject"), }
s.from, }
s.to, return nil
eml.Text,
eml.HTML,
files)
return s.receiveEmail(s.ctx, email)
} }
func (s *incomingSession) Reset() {} func (s *incomingSession) Reset() {}
func (s *incomingSession) Logout() error { return nil } func (s *incomingSession) Logout() error { return nil }
// outgoingSession represents an SMTP-submission session sending emails from external scripts, using postmoogle as SMTP server // outgoingSession represents an SMTP-submission session sending emails from external scripts, using postmoogle as SMTP server
type outgoingSession struct { type outgoingSession struct {
log *logger.Logger log *logger.Logger
sendmail func(string, string, string) error sendmail func(string, string, string) error
privkey string privkey string
domains []string domains []string
getRoomID func(string) (id.RoomID, bool)
ctx context.Context ctx context.Context
to string tos []string
from string from string
fromRoom id.RoomID
} }
func (s *outgoingSession) Mail(from string, opts smtp.MailOptions) error { func (s *outgoingSession) Mail(from string, opts smtp.MailOptions) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from) sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
if !utils.AddressValid(from) { if !email.AddressValid(from) {
return errors.New("please, provide email address") return errors.New("please, provide email address")
} }
hostname := utils.Hostname(from)
var domainok bool
for _, domain := range s.domains {
if hostname == domain {
domainok = true
break
}
}
if !domainok {
s.log.Debug("wrong domain of %s", from)
return ErrNoUser
}
roomID, ok := s.getRoomID(utils.Mailbox(from))
if !ok {
s.log.Debug("mapping for %s not found", from)
return ErrNoUser
}
if s.fromRoom != roomID {
s.log.Warn("sender from %q tries to impersonate %q", s.fromRoom, roomID)
return ErrNoUser
}
return nil return nil
} }
func (s *outgoingSession) Rcpt(to string) error { func (s *outgoingSession) Rcpt(to string) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to) sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
s.to = to s.tos = append(s.tos, to)
s.log.Debug("mail to %s", to) s.log.Debug("mail to %s", to)
return nil return nil
@@ -137,49 +210,41 @@ func (s *outgoingSession) Rcpt(to string) error {
func (s *outgoingSession) Data(r io.Reader) error { func (s *outgoingSession) Data(r io.Reader) error {
parser := enmime.NewParser() parser := enmime.NewParser()
eml, err := parser.ReadEnvelope(r) envelope, err := parser.ReadEnvelope(r)
if err != nil { if err != nil {
return err return err
} }
eml := email.FromEnvelope(s.tos[0], envelope)
for _, to := range s.tos {
eml.RcptTo = to
err := s.sendmail(eml.From, to, eml.Compose(s.privkey))
if err != nil {
return err
}
}
files := parseAttachments(eml.Attachments, s.log) return nil
email := utils.NewEmail(
eml.GetHeader("Message-Id"),
eml.GetHeader("In-Reply-To"),
eml.GetHeader("References"),
eml.GetHeader("Subject"),
s.from,
s.to,
eml.Text,
eml.HTML,
files)
return s.sendmail(email.From, email.To, email.Compose(s.privkey))
} }
func (s *outgoingSession) Reset() {} func (s *outgoingSession) Reset() {}
func (s *outgoingSession) Logout() error { return nil } func (s *outgoingSession) Logout() error { return nil }
func validateEmail(from, to string, log *logger.Logger, options utils.IncomingFilteringOptions) bool { func validateIncoming(from, to string, senderAddr net.Addr, log *logger.Logger, options email.IncomingFilteringOptions) bool {
var sender net.IP
switch netaddr := senderAddr.(type) {
case *net.TCPAddr:
sender = netaddr.IP
default:
host, _, _ := net.SplitHostPort(senderAddr.String()) // nolint:errcheck
sender = net.ParseIP(host)
}
enforce := validator.Enforce{ enforce := validator.Enforce{
Email: true, Email: true,
MX: options.SpamcheckMX(), MX: options.SpamcheckMX(),
SPF: options.SpamcheckSPF(),
SMTP: options.SpamcheckSMTP(), SMTP: options.SpamcheckSMTP(),
} }
v := validator.New(options.Spamlist(), enforce, to, log) v := validator.New(options.Spamlist(), enforce, to, log)
return v.Email(from) return v.Email(from, sender)
}
func parseAttachments(parts []*enmime.Part, log *logger.Logger) []*utils.File {
files := make([]*utils.File, 0, len(parts))
for _, attachment := range parts {
for _, err := range attachment.Errors {
log.Warn("attachment error: %v", err)
}
file := utils.NewFile(attachment.FileName, attachment.Content)
files = append(files, file)
}
return files
} }

View File

@@ -1,44 +0,0 @@
package utils
import (
"bytes"
"strings"
"golang.org/x/net/html"
)
// StripHTMLTag from text
//
// Source: https://siongui.github.io/2018/01/16/go-remove-html-inline-style/
func StripHTMLTag(text, tag string) (string, error) {
doc, err := html.Parse(strings.NewReader(text))
if err != nil {
return "", err
}
stripHTMLTag(doc, tag)
var out bytes.Buffer
err = html.Render(&out, doc)
if err != nil {
return "", err
}
return out.String(), nil
}
func stripHTMLTag(node *html.Node, tag string) {
i := -1
for index, attr := range node.Attr {
if attr.Key == tag {
i = index
break
}
}
if i != -1 {
node.Attr = append(node.Attr[:i], node.Attr[i+1:]...)
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
stripHTMLTag(child, tag)
}
}

41
utils/mail.go Normal file
View File

@@ -0,0 +1,41 @@
package utils
import "strings"
// Mailbox returns mailbox part from email address
func Mailbox(email string) string {
index := strings.LastIndex(email, "@")
if index == -1 {
return email
}
return email[:index]
}
// EmailsList returns human-readable list of mailbox's emails for all available domains
func EmailsList(mailbox string, domain string) string {
var msg strings.Builder
domain = SanitizeDomain(domain)
msg.WriteString(mailbox)
msg.WriteString("@")
msg.WriteString(domain)
count := len(domains) - 1
for i, aliasDomain := range domains {
if i < count {
msg.WriteString(", ")
}
if aliasDomain == domain {
continue
}
msg.WriteString(mailbox)
msg.WriteString("@")
msg.WriteString(aliasDomain)
}
return msg.String()
}
// Hostname returns hostname part from email address
func Hostname(email string) string {
return email[strings.LastIndex(email, "@")+1:]
}

View File

@@ -32,16 +32,17 @@ func EventParent(currentID id.EventID, content *event.MessageEventContent) id.Ev
return currentID return currentID
} }
if content.GetRelatesTo() == nil { relation := content.OptionalGetRelatesTo()
if relation == nil {
return currentID return currentID
} }
threadParent := content.RelatesTo.GetThreadParent() threadParent := relation.GetThreadParent()
if threadParent != "" { if threadParent != "" {
return threadParent return threadParent
} }
replyParent := content.RelatesTo.GetReplyTo() replyParent := relation.GetReplyTo()
if replyParent != "" { if replyParent != "" {
return replyParent return replyParent
} }
@@ -50,7 +51,7 @@ func EventParent(currentID id.EventID, content *event.MessageEventContent) id.Ev
} }
// EventField returns field value from raw event content // EventField returns field value from raw event content
func EventField[T comparable](content *event.Content, field string) T { func EventField[T any](content *event.Content, field string) T {
var zero T var zero T
raw := content.Raw[field] raw := content.Raw[field]
if raw == nil { if raw == nil {

32
utils/mutex.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import "sync"
// Mutex map
type Mutex map[string]*sync.Mutex
// NewMutex map
func NewMutex() Mutex {
return Mutex{}
}
// Lock by key
func (m Mutex) Lock(key string) {
_, ok := m[key]
if !ok {
m[key] = &sync.Mutex{}
}
m[key].Lock()
}
// Unlock by key
func (m Mutex) Unlock(key string) {
_, ok := m[key]
if !ok {
return
}
m[key].Unlock()
delete(m, key)
}

View File

@@ -1,6 +1,7 @@
package utils package utils
import ( import (
"net"
"strconv" "strconv"
"strings" "strings"
@@ -22,42 +23,14 @@ func SetDomains(slice []string) {
domains = slice domains = slice
} }
// Mailbox returns mailbox part from email address // AddrIP returns IP from a network address
func Mailbox(email string) string { func AddrIP(addr net.Addr) string {
index := strings.LastIndex(email, "@") key := addr.String()
if index == -1 { host, _, _ := net.SplitHostPort(key) //nolint:errcheck // either way it's ok
return email if host != "" {
key = host
} }
return email[:index] return key
}
// EmailsList returns human-readable list of mailbox's emails for all available domains
func EmailsList(mailbox string, domain string) string {
var msg strings.Builder
domain = SanitizeDomain(domain)
msg.WriteString(mailbox)
msg.WriteString("@")
msg.WriteString(domain)
count := len(domains) - 1
for i, aliasDomain := range domains {
if i < count {
msg.WriteString(", ")
}
if aliasDomain == domain {
continue
}
msg.WriteString(mailbox)
msg.WriteString("@")
msg.WriteString(aliasDomain)
}
return msg.String()
}
// Hostname returns hostname part from email address
func Hostname(email string) string {
return email[strings.LastIndex(email, "@")+1:]
} }
// SanitizeDomain checks that input domain is available for use // SanitizeDomain checks that input domain is available for use

10
vendor/blitiri.com.ar/go/spf/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Ignore anything beginning with a dot: these are usually temporary or
# unimportant.
.*
# Exceptions to the rule above: files we care about that would otherwise be
# excluded.
!.gitignore
# go-fuzz build artifacts.
*-fuzz.zip

27
vendor/blitiri.com.ar/go/spf/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Licensed under the MIT licence, which is reproduced below (from
https://opensource.org/licenses/MIT).
-----
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
vendor/blitiri.com.ar/go/spf/README.md generated vendored Normal file
View File

@@ -0,0 +1,49 @@
# blitiri.com.ar/go/spf
[![GoDoc](https://godoc.org/blitiri.com.ar/go/spf?status.svg)](https://pkg.go.dev/blitiri.com.ar/go/spf)
[![Build Status](https://gitlab.com/albertito/spf/badges/master/pipeline.svg)](https://gitlab.com/albertito/spf/-/pipelines)
[![Go Report Card](https://goreportcard.com/badge/github.com/albertito/spf)](https://goreportcard.com/report/github.com/albertito/spf)
[![Coverage Status](https://coveralls.io/repos/github/albertito/spf/badge.svg?branch=next)](https://coveralls.io/github/albertito/spf)
[spf](https://godoc.org/blitiri.com.ar/go/spf) is an open source
implementation of the [Sender Policy Framework
(SPF)](https://en.wikipedia.org/wiki/Sender_Policy_Framework) in Go.
It is used by the [chasquid](https://blitiri.com.ar/p/chasquid/) and
[maddy](https://maddy.email) SMTP servers.
## Example
```go
// Check if `sender` is authorized to send from the given `ip`. The `domain`
// is used if the sender doesn't have one.
result, err := spf.CheckHostWithSender(ip, domain, sender)
if result == spf.Fail {
// Not authorized to send.
}
```
See the [package documentation](https://pkg.go.dev/blitiri.com.ar/go/spf) for
more details.
## Status
All SPF mechanisms, modifiers, and macros are supported.
The API should be considered stable. Major version changes will be announced
to the mailing list (details below).
## Contact
If you have any questions, comments or patches please send them to the mailing
list, `chasquid@googlegroups.com`.
To subscribe, send an email to `chasquid+subscribe@googlegroups.com`.
You can also browse the
[archives](https://groups.google.com/forum/#!forum/chasquid).

58
vendor/blitiri.com.ar/go/spf/fuzz.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
// Fuzz testing for package spf.
//
// Run it with:
//
// go-fuzz-build blitiri.com.ar/go/spf
// go-fuzz -bin=./spf-fuzz.zip -workdir=testdata/fuzz
//
//go:build gofuzz
// +build gofuzz
package spf
import (
"net"
"blitiri.com.ar/go/spf/internal/dnstest"
)
// Parsed IP addresses, for convenience.
var (
ip1110 = net.ParseIP("1.1.1.0")
ip1111 = net.ParseIP("1.1.1.1")
ip6666 = net.ParseIP("2001:db8::68")
ip6660 = net.ParseIP("2001:db8::0")
)
// DNS resolver to use. Will be initialized once with the expected fixtures,
// and then reused on each fuzz run.
var dns = dnstest.NewResolver()
func init() {
dns.Ip["d1111"] = []net.IP{ip1111}
dns.Ip["d1110"] = []net.IP{ip1110}
dns.Mx["d1110"] = []*net.MX{{"d1110", 5}, {"nothing", 10}}
dns.Ip["d6666"] = []net.IP{ip6666}
dns.Ip["d6660"] = []net.IP{ip6660}
dns.Mx["d6660"] = []*net.MX{{"d6660", 5}, {"nothing", 10}}
dns.Addr["2001:db8::68"] = []string{"sonlas6.", "domain.", "d6666."}
dns.Addr["1.1.1.1"] = []string{"lalala.", "domain.", "d1111."}
}
func Fuzz(data []byte) int {
// The domain's TXT record comes from the fuzzer.
dns.Txt["domain"] = []string{string(data)}
v4result, _ := CheckHostWithSender(
ip1111, "helo", "domain", WithResolver(dns))
v6result, _ := CheckHostWithSender(
ip6666, "helo", "domain", WithResolver(dns))
// Raise priority if any of the results was something other than
// PermError, as it means the data was better formed.
if v4result != PermError || v6result != PermError {
return 1
}
return 0
}

111
vendor/blitiri.com.ar/go/spf/internal/dnstest/dns.go generated vendored Normal file
View File

@@ -0,0 +1,111 @@
// DNS resolver for testing purposes.
//
// In the future, when go fuzz can make use of _test.go files, we can rename
// this file dns_test.go and remove this extra package entirely.
// Until then, unfortunately this is the most reasonable way to share these
// helpers between go and fuzz tests.
package dnstest
import (
"context"
"net"
"strings"
)
// Testing DNS resolver.
//
// Not exported since this is not part of the public API and only used
// internally on tests.
//
type TestResolver struct {
Txt map[string][]string
Mx map[string][]*net.MX
Ip map[string][]net.IP
Addr map[string][]string
Cname map[string]string
Errors map[string]error
}
func NewResolver() *TestResolver {
return &TestResolver{
Txt: map[string][]string{},
Mx: map[string][]*net.MX{},
Ip: map[string][]net.IP{},
Addr: map[string][]string{},
Cname: map[string]string{},
Errors: map[string]error{},
}
}
var nxDomainErr = &net.DNSError{
Err: "domain not found (for testing)",
IsNotFound: true,
}
func (r *TestResolver) LookupTXT(ctx context.Context, domain string) (txts []string, err error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
domain = strings.ToLower(domain)
domain = strings.TrimRight(domain, ".")
if cname, ok := r.Cname[domain]; ok {
return r.LookupTXT(ctx, cname)
}
if _, ok := r.Txt[domain]; !ok && r.Errors[domain] == nil {
return nil, nxDomainErr
}
return r.Txt[domain], r.Errors[domain]
}
func (r *TestResolver) LookupMX(ctx context.Context, domain string) (mxs []*net.MX, err error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
domain = strings.ToLower(domain)
domain = strings.TrimRight(domain, ".")
if cname, ok := r.Cname[domain]; ok {
return r.LookupMX(ctx, cname)
}
if _, ok := r.Mx[domain]; !ok && r.Errors[domain] == nil {
return nil, nxDomainErr
}
return r.Mx[domain], r.Errors[domain]
}
func (r *TestResolver) LookupIPAddr(ctx context.Context, host string) (as []net.IPAddr, err error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
host = strings.ToLower(host)
host = strings.TrimRight(host, ".")
if cname, ok := r.Cname[host]; ok {
return r.LookupIPAddr(ctx, cname)
}
if _, ok := r.Ip[host]; !ok && r.Errors[host] == nil {
return nil, nxDomainErr
}
return ipsToAddrs(r.Ip[host]), r.Errors[host]
}
func ipsToAddrs(ips []net.IP) []net.IPAddr {
as := []net.IPAddr{}
for _, ip := range ips {
as = append(as, net.IPAddr{IP: ip, Zone: ""})
}
return as
}
func (r *TestResolver) LookupAddr(ctx context.Context, host string) (addrs []string, err error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
host = strings.ToLower(host)
host = strings.TrimRight(host, ".")
if cname, ok := r.Cname[host]; ok {
return r.LookupAddr(ctx, cname)
}
if _, ok := r.Addr[host]; !ok && r.Errors[host] == nil {
return nil, nxDomainErr
}
return r.Addr[host], r.Errors[host]
}

1044
vendor/blitiri.com.ar/go/spf/spf.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

12
vendor/github.com/fsnotify/fsnotify/.editorconfig generated vendored Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*.go]
indent_style = tab
indent_size = 4
insert_final_newline = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

1
vendor/github.com/fsnotify/fsnotify/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1 @@
go.sum linguist-generated

6
vendor/github.com/fsnotify/fsnotify/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,6 @@
# go test -c output
*.test
*.test.exe
# Output of go build ./cmd/fsnotify
/fsnotify

2
vendor/github.com/fsnotify/fsnotify/.mailmap generated vendored Normal file
View File

@@ -0,0 +1,2 @@
Chris Howey <howeyc@gmail.com> <chris@howey.me>
Nathan Youngman <git@nathany.com> <4566+nathany@users.noreply.github.com>

470
vendor/github.com/fsnotify/fsnotify/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,470 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
Nothing yet.
## [1.6.0] - 2022-10-13
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
but not documented). It also increases the minimum Linux version to 2.6.32.
### Additions
- all: add `Event.Has()` and `Op.Has()` ([#477])
This makes checking events a lot easier; for example:
if event.Op&Write == Write && !(event.Op&Remove == Remove) {
}
Becomes:
if event.Has(Write) && !event.Has(Remove) {
}
- all: add cmd/fsnotify ([#463])
A command-line utility for testing and some examples.
### Changes and fixes
- inotify: don't ignore events for files that don't exist ([#260], [#470])
Previously the inotify watcher would call `os.Lstat()` to check if a file
still exists before emitting events.
This was inconsistent with other platforms and resulted in inconsistent event
reporting (e.g. when a file is quickly removed and re-created), and generally
a source of confusion. It was added in 2013 to fix a memory leak that no
longer exists.
- all: return `ErrNonExistentWatch` when `Remove()` is called on a path that's
not watched ([#460])
- inotify: replace epoll() with non-blocking inotify ([#434])
Non-blocking inotify was not generally available at the time this library was
written in 2014, but now it is. As a result, the minimum Linux version is
bumped from 2.6.27 to 2.6.32. This hugely simplifies the code and is faster.
- kqueue: don't check for events every 100ms ([#480])
The watcher would wake up every 100ms, even when there was nothing to do. Now
it waits until there is something to do.
- macos: retry opening files on EINTR ([#475])
- kqueue: skip unreadable files ([#479])
kqueue requires a file descriptor for every file in a directory; this would
fail if a file was unreadable by the current user. Now these files are simply
skipped.
- windows: fix renaming a watched directory if the parent is also watched ([#370])
- windows: increase buffer size from 4K to 64K ([#485])
- windows: close file handle on Remove() ([#288])
- kqueue: put pathname in the error if watching a file fails ([#471])
- inotify, windows: calling Close() more than once could race ([#465])
- kqueue: improve Close() performance ([#233])
- all: various documentation additions and clarifications.
[#233]: https://github.com/fsnotify/fsnotify/pull/233
[#260]: https://github.com/fsnotify/fsnotify/pull/260
[#288]: https://github.com/fsnotify/fsnotify/pull/288
[#370]: https://github.com/fsnotify/fsnotify/pull/370
[#434]: https://github.com/fsnotify/fsnotify/pull/434
[#460]: https://github.com/fsnotify/fsnotify/pull/460
[#463]: https://github.com/fsnotify/fsnotify/pull/463
[#465]: https://github.com/fsnotify/fsnotify/pull/465
[#470]: https://github.com/fsnotify/fsnotify/pull/470
[#471]: https://github.com/fsnotify/fsnotify/pull/471
[#475]: https://github.com/fsnotify/fsnotify/pull/475
[#477]: https://github.com/fsnotify/fsnotify/pull/477
[#479]: https://github.com/fsnotify/fsnotify/pull/479
[#480]: https://github.com/fsnotify/fsnotify/pull/480
[#485]: https://github.com/fsnotify/fsnotify/pull/485
## [1.5.4] - 2022-04-25
* Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447)
* go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444)
* Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443)
## [1.5.3] - 2022-04-22
* This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445)
## [1.5.2] - 2022-04-21
* Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374)
* Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361)
* Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424)
* Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406)
* fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416)
## [1.5.1] - 2021-08-24
* Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394)
## [1.5.0] - 2021-08-20
* Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381)
* Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298)
* Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289)
* CI: Use GitHub Actions for CI and cover go 1.12-1.17
[#378](https://github.com/fsnotify/fsnotify/pull/378)
[#381](https://github.com/fsnotify/fsnotify/pull/381)
[#385](https://github.com/fsnotify/fsnotify/pull/385)
* Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325)
## [1.4.9] - 2020-03-11
* Move example usage to the readme #329. This may resolve #328.
## [1.4.8] - 2020-03-10
* CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216)
* Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265)
* Tests: t.Fatalf -> t.Errorf in go routines (@gdey #266)
* CI: Less verbosity (@nathany #267)
* Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267)
* Tests: Check if channels are closed in the example (@alexeykazakov #244)
* CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284)
* CI: Add windows to travis matrix (@cpuguy83 #284)
* Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93)
* Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219)
* Linux: open files with close-on-exec (@linxiulei #273)
* Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 )
* Project: Add go.mod (@nathany #309)
* Project: Revise editor config (@nathany #309)
* Project: Update copyright for 2019 (@nathany #309)
* CI: Drop go1.8 from CI matrix (@nathany #309)
* Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e )
## [1.4.7] - 2018-01-09
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
* Tests: Fix missing verb on format string (thanks @rchiossi)
* Linux: Fix deadlock in Remove (thanks @aarondl)
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne)
* Docs: Moved FAQ into the README (thanks @vahe)
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich)
* Docs: replace references to OS X with macOS
## [1.4.2] - 2016-10-10
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack)
## [1.4.1] - 2016-10-04
* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack)
## [1.4.0] - 2016-10-01
* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie)
## [1.3.1] - 2016-06-28
* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc)
## [1.3.0] - 2016-04-19
* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135)
## [1.2.10] - 2016-03-02
* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj)
## [1.2.9] - 2016-01-13
kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep)
## [1.2.8] - 2015-12-17
* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test)
* inotify: fix race in test
* enable race detection for continuous integration (Linux, Mac, Windows)
## [1.2.5] - 2015-10-17
* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki)
* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken)
* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie)
* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion)
## [1.2.1] - 2015-10-14
* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx)
## [1.2.0] - 2015-02-08
* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD)
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD)
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59)
## [1.1.1] - 2015-02-05
* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD)
## [1.1.0] - 2014-12-12
* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43)
* add low-level functions
* only need to store flags on directories
* less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13)
* done can be an unbuffered channel
* remove calls to os.NewSyscallError
* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher)
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48)
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
## [1.0.4] - 2014-09-07
* kqueue: add dragonfly to the build tags.
* Rename source code files, rearrange code so exported APIs are at the top.
* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang)
## [1.0.3] - 2014-08-19
* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36)
## [1.0.2] - 2014-08-17
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
* [Fix] Make ./path and path equivalent. (thanks @zhsso)
## [1.0.0] - 2014-08-15
* [API] Remove AddWatch on Windows, use Add.
* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30)
* Minor updates based on feedback from golint.
## dev / 2014-07-09
* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify).
* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno)
## dev / 2014-07-04
* kqueue: fix incorrect mutex used in Close()
* Update example to demonstrate usage of Op.
## dev / 2014-06-28
* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4)
* Fix for String() method on Event (thanks Alex Brainman)
* Don't build on Plan 9 or Solaris (thanks @4ad)
## dev / 2014-06-21
* Events channel of type Event rather than *Event.
* [internal] use syscall constants directly for inotify and kqueue.
* [internal] kqueue: rename events to kevents and fileEvent to event.
## dev / 2014-06-19
* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally).
* [internal] remove cookie from Event struct (unused).
* [internal] Event struct has the same definition across every OS.
* [internal] remove internal watch and removeWatch methods.
## dev / 2014-06-12
* [API] Renamed Watch() to Add() and RemoveWatch() to Remove().
* [API] Pluralized channel names: Events and Errors.
* [API] Renamed FileEvent struct to Event.
* [API] Op constants replace methods like IsCreate().
## dev / 2014-06-12
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
## dev / 2014-05-23
* [API] Remove current implementation of WatchFlags.
* current implementation doesn't take advantage of OS for efficiency
* provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes
* no tests for the current implementation
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195)
## [0.9.3] - 2014-12-31
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
## [0.9.2] - 2014-08-17
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
## [0.9.1] - 2014-06-12
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
## [0.9.0] - 2014-01-17
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany)
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare)
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library.
## [0.8.12] - 2013-11-13
* [API] Remove FD_SET and friends from Linux adapter
## [0.8.11] - 2013-11-02
* [Doc] Add Changelog [#72][] (thanks @nathany)
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond)
## [0.8.10] - 2013-10-19
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott)
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer)
* [Doc] specify OS-specific limits in README (thanks @debrando)
## [0.8.9] - 2013-09-08
* [Doc] Contributing (thanks @nathany)
* [Doc] update package path in example code [#63][] (thanks @paulhammond)
* [Doc] GoCI badge in README (Linux only) [#60][]
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany)
## [0.8.8] - 2013-06-17
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie)
## [0.8.7] - 2013-06-03
* [API] Make syscall flags internal
* [Fix] inotify: ignore event changes
* [Fix] race in symlink test [#45][] (reported by @srid)
* [Fix] tests on Windows
* lower case error messages
## [0.8.6] - 2013-05-23
* kqueue: Use EVT_ONLY flag on Darwin
* [Doc] Update README with full example
## [0.8.5] - 2013-05-09
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg)
## [0.8.4] - 2013-04-07
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz)
## [0.8.3] - 2013-03-13
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin)
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin)
## [0.8.2] - 2013-02-07
* [Doc] add Authors
* [Fix] fix data races for map access [#29][] (thanks @fsouza)
## [0.8.1] - 2013-01-09
* [Fix] Windows path separators
* [Doc] BSD License
## [0.8.0] - 2012-11-09
* kqueue: directory watching improvements (thanks @vmirage)
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto)
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr)
## [0.7.4] - 2012-10-09
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji)
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig)
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig)
* [Fix] kqueue: modify after recreation of file
## [0.7.3] - 2012-09-27
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage)
* [Fix] kqueue: no longer get duplicate CREATE events
## [0.7.2] - 2012-09-01
* kqueue: events for created directories
## [0.7.1] - 2012-07-14
* [Fix] for renaming files
## [0.7.0] - 2012-07-02
* [Feature] FSNotify flags
* [Fix] inotify: Added file name back to event path
## [0.6.0] - 2012-06-06
* kqueue: watch files after directory created (thanks @tmc)
## [0.5.1] - 2012-05-22
* [Fix] inotify: remove all watches before Close()
## [0.5.0] - 2012-05-03
* [API] kqueue: return errors during watch instead of sending over channel
* kqueue: match symlink behavior on Linux
* inotify: add `DELETE_SELF` (requested by @taralx)
* [Fix] kqueue: handle EINTR (reported by @robfig)
* [Doc] Godoc example [#1][] (thanks @davecheney)
## [0.4.0] - 2012-03-30
* Go 1 released: build with go tool
* [Feature] Windows support using winfsnotify
* Windows does not have attribute change notifications
* Roll attribute notifications into IsModify
## [0.3.0] - 2012-02-19
* kqueue: add files when watch directory
## [0.2.0] - 2011-12-30
* update to latest Go weekly code
## [0.1.0] - 2011-10-19
* kqueue: add watch on file creation to match inotify
* kqueue: create file event
* inotify: ignore `IN_IGNORED` events
* event String()
* linux: common FileEvent functions
* initial commit
[#79]: https://github.com/howeyc/fsnotify/pull/79
[#77]: https://github.com/howeyc/fsnotify/pull/77
[#72]: https://github.com/howeyc/fsnotify/issues/72
[#71]: https://github.com/howeyc/fsnotify/issues/71
[#70]: https://github.com/howeyc/fsnotify/issues/70
[#63]: https://github.com/howeyc/fsnotify/issues/63
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#60]: https://github.com/howeyc/fsnotify/issues/60
[#59]: https://github.com/howeyc/fsnotify/issues/59
[#49]: https://github.com/howeyc/fsnotify/issues/49
[#45]: https://github.com/howeyc/fsnotify/issues/45
[#40]: https://github.com/howeyc/fsnotify/issues/40
[#36]: https://github.com/howeyc/fsnotify/issues/36
[#33]: https://github.com/howeyc/fsnotify/issues/33
[#29]: https://github.com/howeyc/fsnotify/issues/29
[#25]: https://github.com/howeyc/fsnotify/issues/25
[#24]: https://github.com/howeyc/fsnotify/issues/24
[#21]: https://github.com/howeyc/fsnotify/issues/21

26
vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,26 @@
Thank you for your interest in contributing to fsnotify! We try to review and
merge PRs in a reasonable timeframe, but please be aware that:
- To avoid "wasted" work, please discus changes on the issue tracker first. You
can just send PRs, but they may end up being rejected for one reason or the
other.
- fsnotify is a cross-platform library, and changes must work reasonably well on
all supported platforms.
- Changes will need to be compatible; old code should still compile, and the
runtime behaviour can't change in ways that are likely to lead to problems for
users.
Testing
-------
Just `go test ./...` runs all the tests; the CI runs this on all supported
platforms. Testing different platforms locally can be done with something like
[goon] or [Vagrant], but this isn't super-easy to set up at the moment.
Use the `-short` flag to make the "stress test" run faster.
[goon]: https://github.com/arp242/goon
[Vagrant]: https://www.vagrantup.com/
[integration_test.go]: /integration_test.go

25
vendor/github.com/fsnotify/fsnotify/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,25 @@
Copyright © 2012 The Go Authors. All rights reserved.
Copyright © fsnotify Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of Google Inc. nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

161
vendor/github.com/fsnotify/fsnotify/README.md generated vendored Normal file
View File

@@ -0,0 +1,161 @@
fsnotify is a Go library to provide cross-platform filesystem notifications on
Windows, Linux, macOS, and BSD systems.
Go 1.16 or newer is required; the full documentation is at
https://pkg.go.dev/github.com/fsnotify/fsnotify
**It's best to read the documentation at pkg.go.dev, as it's pinned to the last
released version, whereas this README is for the last development version which
may include additions/changes.**
---
Platform support:
| Adapter | OS | Status |
| --------------------- | ---------------| -------------------------------------------------------------|
| inotify | Linux 2.6.32+ | Supported |
| kqueue | BSD, macOS | Supported |
| ReadDirectoryChangesW | Windows | Supported |
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) |
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/pull/371) |
| fanotify | Linux 5.9+ | [Maybe](https://github.com/fsnotify/fsnotify/issues/114) |
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) |
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) |
Linux and macOS should include Android and iOS, but these are currently untested.
Usage
-----
A basic example:
```go
package main
import (
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// Create new watcher.
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Start listening for events.
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Println("event:", event)
if event.Has(fsnotify.Write) {
log.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
// Add a path.
err = watcher.Add("/tmp")
if err != nil {
log.Fatal(err)
}
// Block main goroutine forever.
<-make(chan struct{})
}
```
Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be
run with:
% go run ./cmd/fsnotify
FAQ
---
### Will a file still be watched when it's moved to another directory?
No, not unless you are watching the location it was moved to.
### Are subdirectories watched too?
No, you must add watches for any directory you want to watch (a recursive
watcher is on the roadmap: [#18]).
[#18]: https://github.com/fsnotify/fsnotify/issues/18
### Do I have to watch the Error and Event channels in a goroutine?
As of now, yes (you can read both channels in the same goroutine using `select`,
you don't need a separate goroutine for both channels; see the example).
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
fsnotify requires support from underlying OS to work. The current NFS and SMB
protocols does not provide network level support for file notifications, and
neither do the /proc and /sys virtual filesystems.
This could be fixed with a polling watcher ([#9]), but it's not yet implemented.
[#9]: https://github.com/fsnotify/fsnotify/issues/9
Platform-specific notes
-----------------------
### Linux
When a file is removed a REMOVE event won't be emitted until all file
descriptors are closed; it will emit a CHMOD instead:
fp := os.Open("file")
os.Remove("file") // CHMOD
fp.Close() // REMOVE
This is the event that inotify sends, so not much can be changed about this.
The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for
the number of watches per user, and `fs.inotify.max_user_instances` specifies
the maximum number of inotify instances per user. Every Watcher you create is an
"instance", and every path you add is a "watch".
These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and
`/proc/sys/fs/inotify/max_user_instances`
To increase them you can use `sysctl` or write the value to proc file:
# The default values on Linux 5.18
sysctl fs.inotify.max_user_watches=124983
sysctl fs.inotify.max_user_instances=128
To make the changes persist on reboot edit `/etc/sysctl.conf` or
`/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your
distro's documentation):
fs.inotify.max_user_watches=124983
fs.inotify.max_user_instances=128
Reaching the limit will result in a "no space left on device" or "too many open
files" error.
### kqueue (macOS, all BSD systems)
kqueue requires opening a file descriptor for every file that's being watched;
so if you're watching a directory with five files then that's six file
descriptors. You will run in to your system's "max open files" limit faster on
these platforms.
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
control the maximum number of open files.
### macOS
Spotlight indexing on macOS can result in multiple events (see [#15]). A temporary
workaround is to add your folder(s) to the *Spotlight Privacy settings* until we
have a native FSEvents implementation (see [#11]).
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#15]: https://github.com/fsnotify/fsnotify/issues/15

162
vendor/github.com/fsnotify/fsnotify/backend_fen.go generated vendored Normal file
View File

@@ -0,0 +1,162 @@
//go:build solaris
// +build solaris
package fsnotify
import (
"errors"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # macOS notes
//
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
// Settings" until we have a native FSEvents implementation (see [#11]).
//
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you
// probably want to wait until you've stopped receiving
// them (see the dedup example in cmd/fsnotify).
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows
// it's never sent.
Events chan Event
// Errors sends any errors.
Errors chan error
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
return nil
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; attempting to watch it more than once will
// return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted.
//
// A path will remain watched if it gets renamed to somewhere else on the same
// filesystem, but the monitor will get removed if the path gets deleted and
// re-created, or if it's moved to a different filesystem.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing
// to the file a temporary file will be written to first, and if successful the
// temporary file is moved to to destination removing the original, or some
// variant thereof. The watcher on the original file is now lost, as it no
// longer exists.
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
return nil
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error {
return nil
}

459
vendor/github.com/fsnotify/fsnotify/backend_inotify.go generated vendored Normal file
View File

@@ -0,0 +1,459 @@
//go:build linux
// +build linux
package fsnotify
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # macOS notes
//
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
// Settings" until we have a native FSEvents implementation (see [#11]).
//
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you
// probably want to wait until you've stopped receiving
// them (see the dedup example in cmd/fsnotify).
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows
// it's never sent.
Events chan Event
// Errors sends any errors.
Errors chan error
// Store fd here as os.File.Read() will no longer return on close after
// calling Fd(). See: https://github.com/golang/go/issues/26439
fd int
mu sync.Mutex // Map access
inotifyFile *os.File
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneResp chan struct{} // Channel to respond to Close
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
// Create inotify fd
// Need to set the FD to nonblocking mode in order for SetDeadline methods to work
// Otherwise, blocking i/o operations won't terminate on close
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 {
return nil, errno
}
w := &Watcher{
fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""),
watches: make(map[string]*watch),
paths: make(map[int]string),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *Watcher) sendEvent(e Event) bool {
select {
case w.Events <- e:
return true
case <-w.done:
}
return false
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
select {
case w.Errors <- err:
return true
case <-w.done:
return false
}
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
w.mu.Lock()
if w.isClosed() {
w.mu.Unlock()
return nil
}
// Send 'close' signal to goroutine, and set the Watcher to closed.
close(w.done)
w.mu.Unlock()
// Causes any blocking reads to return with an error, provided the file
// still supports deadline operations.
err := w.inotifyFile.Close()
if err != nil {
return err
}
// Wait for goroutine to close
<-w.doneResp
return nil
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; attempting to watch it more than once will
// return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted.
//
// A path will remain watched if it gets renamed to somewhere else on the same
// filesystem, but the monitor will get removed if the path gets deleted and
// re-created, or if it's moved to a different filesystem.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing
// to the file a temporary file will be written to first, and if successful the
// temporary file is moved to to destination removing the original, or some
// variant thereof. The watcher on the original file is now lost, as it no
// longer exists.
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
name = filepath.Clean(name)
if w.isClosed() {
return errors.New("inotify instance already closed")
}
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
w.mu.Lock()
defer w.mu.Unlock()
watchEntry := w.watches[name]
if watchEntry != nil {
flags |= watchEntry.flags | unix.IN_MASK_ADD
}
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return errno
}
if watchEntry == nil {
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = name
} else {
watchEntry.wd = uint32(wd)
watchEntry.flags = flags
}
return nil
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name)
// Fetch the watch.
w.mu.Lock()
defer w.mu.Unlock()
watch, ok := w.watches[name]
// Remove it from inotify.
if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
// We successfully removed the watch if InotifyRmWatch doesn't return an
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
delete(w.paths, int(watch.wd))
delete(w.watches, name)
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
// by another thread and we have not received IN_IGNORE event.
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case;
// The only two possible errors are:
//
// - EBADF, which happens when w.fd is not a valid file descriptor
// of any kind.
// - EINVAL, which is when fd is not an inotify descriptor or wd
// is not a valid watch descriptor. Watch descriptors are
// invalidated when they are removed explicitly or implicitly;
// explicitly by inotify_rm_watch, implicitly when the file they
// are watching is deleted.
return errno
}
return nil
}
// WatchList returns all paths added with [Add] (and are not yet removed).
func (w *Watcher) WatchList() []string {
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.watches))
for pathname := range w.watches {
entries = append(entries, pathname)
}
return entries
}
type watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
}
// readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Events channel
func (w *Watcher) readEvents() {
defer func() {
close(w.doneResp)
close(w.Errors)
close(w.Events)
}()
var (
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
errno error // Syscall errno
)
for {
// See if we have been closed.
if w.isClosed() {
return
}
n, err := w.inotifyFile.Read(buf[:])
switch {
case errors.Unwrap(err) == os.ErrClosed:
return
case err != nil:
if !w.sendError(err) {
return
}
continue
}
if n < unix.SizeofInotifyEvent {
var err error
if n == 0 {
// If EOF is received. This should really never happen.
err = io.EOF
} else if n < 0 {
// If an error occurred while reading.
err = errno
} else {
// Read was too short.
err = errors.New("notify: short read in readEvents()")
}
if !w.sendError(err) {
return
}
continue
}
var offset uint32
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
for offset <= uint32(n-unix.SizeofInotifyEvent) {
var (
// Point "raw" to the event in the buffer
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask = uint32(raw.Mask)
nameLen = uint32(raw.Len)
)
if mask&unix.IN_Q_OVERFLOW != 0 {
if !w.sendError(ErrEventOverflow) {
return
}
}
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
w.mu.Lock()
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
w.mu.Unlock()
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
event := w.newEvent(name, mask)
// Send the events that are not ignored on the events channel
if mask&unix.IN_IGNORED == 0 {
if !w.sendEvent(event) {
return
}
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + nameLen
}
}
}
// newEvent returns an platform-independent Event based on an inotify mask.
func (w *Watcher) newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
e.Op |= Create
}
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE {
e.Op |= Remove
}
if mask&unix.IN_MODIFY == unix.IN_MODIFY {
e.Op |= Write
}
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
e.Op |= Rename
}
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
e.Op |= Chmod
}
return e
}

707
vendor/github.com/fsnotify/fsnotify/backend_kqueue.go generated vendored Normal file
View File

@@ -0,0 +1,707 @@
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
// +build freebsd openbsd netbsd dragonfly darwin
package fsnotify
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"golang.org/x/sys/unix"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # macOS notes
//
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
// Settings" until we have a native FSEvents implementation (see [#11]).
//
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you
// probably want to wait until you've stopped receiving
// them (see the dedup example in cmd/fsnotify).
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows
// it's never sent.
Events chan Event
// Errors sends any errors.
Errors chan error
done chan struct{}
kq int // File descriptor (as returned by the kqueue() syscall).
closepipe [2]int // Pipe used for closing.
mu sync.Mutex // Protects access to watcher data
watches map[string]int // Watched file descriptors (key: path).
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
userWatches map[string]struct{} // Watches added with Watcher.Add()
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
isClosed bool // Set to true when Close() is first called
}
type pathInfo struct {
name string
isDir bool
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
kq, closepipe, err := newKqueue()
if err != nil {
return nil, err
}
w := &Watcher{
kq: kq,
closepipe: closepipe,
watches: make(map[string]int),
watchesByDir: make(map[string]map[int]struct{}),
dirFlags: make(map[string]uint32),
paths: make(map[int]pathInfo),
fileExists: make(map[string]struct{}),
userWatches: make(map[string]struct{}),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
// newKqueue creates a new kernel event queue and returns a descriptor.
//
// This registers a new event on closepipe, which will trigger an event when
// it's closed. This way we can use kevent() without timeout/polling; without
// the closepipe, it would block forever and we wouldn't be able to stop it at
// all.
func newKqueue() (kq int, closepipe [2]int, err error) {
kq, err = unix.Kqueue()
if kq == -1 {
return kq, closepipe, err
}
// Register the close pipe.
err = unix.Pipe(closepipe[:])
if err != nil {
unix.Close(kq)
return kq, closepipe, err
}
// Register changes to listen on the closepipe.
changes := make([]unix.Kevent_t, 1)
// SetKevent converts int to the platform-specific types.
unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ,
unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT)
ok, err := unix.Kevent(kq, changes, nil, nil)
if ok == -1 {
unix.Close(kq)
unix.Close(closepipe[0])
unix.Close(closepipe[1])
return kq, closepipe, err
}
return kq, closepipe, nil
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *Watcher) sendEvent(e Event) bool {
select {
case w.Events <- e:
return true
case <-w.done:
}
return false
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
select {
case w.Errors <- err:
return true
case <-w.done:
}
return false
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
w.isClosed = true
// copy paths to remove while locked
pathsToRemove := make([]string, 0, len(w.watches))
for name := range w.watches {
pathsToRemove = append(pathsToRemove, name)
}
w.mu.Unlock() // Unlock before calling Remove, which also locks
for _, name := range pathsToRemove {
w.Remove(name)
}
// Send "quit" message to the reader goroutine.
unix.Close(w.closepipe[1])
close(w.done)
return nil
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; attempting to watch it more than once will
// return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted.
//
// A path will remain watched if it gets renamed to somewhere else on the same
// filesystem, but the monitor will get removed if the path gets deleted and
// re-created, or if it's moved to a different filesystem.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing
// to the file a temporary file will be written to first, and if successful the
// temporary file is moved to to destination removing the original, or some
// variant thereof. The watcher on the original file is now lost, as it no
// longer exists.
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
w.mu.Lock()
w.userWatches[name] = struct{}{}
w.mu.Unlock()
_, err := w.addWatch(name, noteAllEvents)
return err
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name)
w.mu.Lock()
watchfd, ok := w.watches[name]
w.mu.Unlock()
if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
err := w.register([]int{watchfd}, unix.EV_DELETE, 0)
if err != nil {
return err
}
unix.Close(watchfd)
w.mu.Lock()
isDir := w.paths[watchfd].isDir
delete(w.watches, name)
delete(w.userWatches, name)
parentName := filepath.Dir(name)
delete(w.watchesByDir[parentName], watchfd)
if len(w.watchesByDir[parentName]) == 0 {
delete(w.watchesByDir, parentName)
}
delete(w.paths, watchfd)
delete(w.dirFlags, name)
delete(w.fileExists, name)
w.mu.Unlock()
// Find all watched paths that are in this directory that are not external.
if isDir {
var pathsToRemove []string
w.mu.Lock()
for fd := range w.watchesByDir[name] {
path := w.paths[fd]
if _, ok := w.userWatches[path.name]; !ok {
pathsToRemove = append(pathsToRemove, path.name)
}
}
w.mu.Unlock()
for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error
// to the user, as that will just confuse them with an error about
// a path they did not explicitly watch themselves.
w.Remove(name)
}
}
return nil
}
// WatchList returns all paths added with [Add] (and are not yet removed).
func (w *Watcher) WatchList() []string {
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.userWatches))
for pathname := range w.userWatches {
entries = append(entries, pathname)
}
return entries
}
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
// addWatch adds name to the watched file set.
// The flags are interpreted as described in kevent(2).
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
var isDir bool
// Make ./name and name equivalent
name = filepath.Clean(name)
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return "", errors.New("kevent instance already closed")
}
watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags.
if alreadyWatching {
isDir = w.paths[watchfd].isDir
}
w.mu.Unlock()
if !alreadyWatching {
fi, err := os.Lstat(name)
if err != nil {
return "", err
}
// Don't watch sockets or named pipes
if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) {
return "", nil
}
// Follow Symlinks
//
// Linux can add unresolvable symlinks to the watch list without issue,
// and Windows can't do symlinks period. To maintain consistency, we
// will act like everything is fine if the link can't be resolved.
// There will simply be no file events for broken symlinks. Hence the
// returns of nil on errors.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
name, err = filepath.EvalSymlinks(name)
if err != nil {
return "", nil
}
w.mu.Lock()
_, alreadyWatching = w.watches[name]
w.mu.Unlock()
if alreadyWatching {
return name, nil
}
fi, err = os.Lstat(name)
if err != nil {
return "", nil
}
}
// Retry on EINTR; open() can return EINTR in practice on macOS.
// See #354, and go issues 11180 and 39237.
for {
watchfd, err = unix.Open(name, openMode, 0)
if err == nil {
break
}
if errors.Is(err, unix.EINTR) {
continue
}
return "", err
}
isDir = fi.IsDir()
}
err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags)
if err != nil {
unix.Close(watchfd)
return "", err
}
if !alreadyWatching {
w.mu.Lock()
parentName := filepath.Dir(name)
w.watches[name] = watchfd
watchesByDir, ok := w.watchesByDir[parentName]
if !ok {
watchesByDir = make(map[int]struct{}, 1)
w.watchesByDir[parentName] = watchesByDir
}
watchesByDir[watchfd] = struct{}{}
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock()
}
if isDir {
// Watch the directory if it has not been watched before,
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock()
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE)
// Store flags so this watch can be updated later
w.dirFlags[name] = flags
w.mu.Unlock()
if watchDir {
if err := w.watchDirectoryFiles(name); err != nil {
return "", err
}
}
}
return name, nil
}
// readEvents reads from kqueue and converts the received kevents into
// Event values that it sends down the Events channel.
func (w *Watcher) readEvents() {
defer func() {
err := unix.Close(w.kq)
if err != nil {
w.Errors <- err
}
unix.Close(w.closepipe[0])
close(w.Events)
close(w.Errors)
}()
eventBuffer := make([]unix.Kevent_t, 10)
for closed := false; !closed; {
kevents, err := w.read(eventBuffer)
// EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != unix.EINTR {
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) {
closed = true
}
continue
}
// Flush the events we received to the Events channel
for _, kevent := range kevents {
var (
watchfd = int(kevent.Ident)
mask = uint32(kevent.Fflags)
)
// Shut down the loop when the pipe is closed, but only after all
// other events have been processed.
if watchfd == w.closepipe[0] {
closed = true
continue
}
w.mu.Lock()
path := w.paths[watchfd]
w.mu.Unlock()
event := w.newEvent(path.name, mask)
if path.isDir && !event.Has(Remove) {
// Double check to make sure the directory exists. This can
// happen when we do a rm -fr on a recursively watched folders
// and we receive a modification event first but the folder has
// been deleted and later receive the delete event.
if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
event.Op |= Remove
}
}
if event.Has(Rename) || event.Has(Remove) {
w.Remove(event.Name)
w.mu.Lock()
delete(w.fileExists, event.Name)
w.mu.Unlock()
}
if path.isDir && event.Has(Write) && !event.Has(Remove) {
w.sendDirectoryChangeEvents(event.Name)
} else {
if !w.sendEvent(event) {
closed = true
continue
}
}
if event.Has(Remove) {
// Look for a file that may have overwritten this.
// For example, mv f1 f2 will delete f2, then create f2.
if path.isDir {
fileDir := filepath.Clean(event.Name)
w.mu.Lock()
_, found := w.watches[fileDir]
w.mu.Unlock()
if found {
// make sure the directory exists before we watch for changes. When we
// do a recursive watch and perform rm -fr, the parent directory might
// have gone missing, ignore the missing directory and let the
// upcoming delete event remove the watch from the parent directory.
if _, err := os.Lstat(fileDir); err == nil {
w.sendDirectoryChangeEvents(fileDir)
}
}
} else {
filePath := filepath.Clean(event.Name)
if fileInfo, err := os.Lstat(filePath); err == nil {
w.sendFileCreatedEventIfNew(filePath, fileInfo)
}
}
}
}
}
}
// newEvent returns an platform-independent Event based on kqueue Fflags.
func (w *Watcher) newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE {
e.Op |= Remove
}
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE {
e.Op |= Write
}
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME {
e.Op |= Rename
}
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
e.Op |= Chmod
}
return e
}
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
return err
}
for _, fileInfo := range files {
path := filepath.Join(dirPath, fileInfo.Name())
cleanPath, err := w.internalWatch(path, fileInfo)
if err != nil {
// No permission to read the file; that's not a problem: just skip.
// But do add it to w.fileExists to prevent it from being picked up
// as a "new" file later (it still shows up in the directory
// listing).
switch {
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
cleanPath = filepath.Clean(path)
default:
return fmt.Errorf("%q: %w", filepath.Join(dirPath, fileInfo.Name()), err)
}
}
w.mu.Lock()
w.fileExists[cleanPath] = struct{}{}
w.mu.Unlock()
}
return nil
}
// Search the directory for new files and send an event for them.
//
// This functionality is to have the BSD watcher match the inotify, which sends
// a create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dir string) {
// Get all files
files, err := ioutil.ReadDir(dir)
if err != nil {
if !w.sendError(fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)) {
return
}
}
// Search for new files
for _, fi := range files {
err := w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil {
return
}
}
}
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) {
w.mu.Lock()
_, doesExist := w.fileExists[filePath]
w.mu.Unlock()
if !doesExist {
if !w.sendEvent(Event{Name: filePath, Op: Create}) {
return
}
}
// like watchDirectoryFiles (but without doing another ReadDir)
filePath, err = w.internalWatch(filePath, fileInfo)
if err != nil {
return err
}
w.mu.Lock()
w.fileExists[filePath] = struct{}{}
w.mu.Unlock()
return nil
}
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) {
if fileInfo.IsDir() {
// mimic Linux providing delete events for subdirectories
// but preserve the flags used if currently watching subdirectory
w.mu.Lock()
flags := w.dirFlags[name]
w.mu.Unlock()
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
return w.addWatch(name, flags)
}
// watch file to mimic Linux inotify
return w.addWatch(name, noteAllEvents)
}
// Register events with the queue.
func (w *Watcher) register(fds []int, flags int, fflags uint32) error {
changes := make([]unix.Kevent_t, len(fds))
for i, fd := range fds {
// SetKevent converts int to the platform-specific types.
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags)
changes[i].Fflags = fflags
}
// Register the events.
success, err := unix.Kevent(w.kq, changes, nil, nil)
if success == -1 {
return err
}
return nil
}
// read retrieves pending events, or waits until an event occurs.
func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
n, err := unix.Kevent(w.kq, nil, events, nil)
if err != nil {
return nil, err
}
return events[0:n], nil
}

66
vendor/github.com/fsnotify/fsnotify/backend_other.go generated vendored Normal file
View File

@@ -0,0 +1,66 @@
//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows
// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
package fsnotify
import (
"fmt"
"runtime"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct{}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS)
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
return nil
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; attempting to watch it more than once will
// return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted.
//
// A path will remain watched if it gets renamed to somewhere else on the same
// filesystem, but the monitor will get removed if the path gets deleted and
// re-created, or if it's moved to a different filesystem.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing
// to the file a temporary file will be written to first, and if successful the
// temporary file is moved to to destination removing the original, or some
// variant thereof. The watcher on the original file is now lost, as it no
// longer exists.
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
return nil
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error {
return nil
}

746
vendor/github.com/fsnotify/fsnotify/backend_windows.go generated vendored Normal file
View File

@@ -0,0 +1,746 @@
//go:build windows
// +build windows
package fsnotify
import (
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/windows"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # macOS notes
//
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
// Settings" until we have a native FSEvents implementation (see [#11]).
//
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you
// probably want to wait until you've stopped receiving
// them (see the dedup example in cmd/fsnotify).
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows
// it's never sent.
Events chan Event
// Errors sends any errors.
Errors chan error
port windows.Handle // Handle to completion port
input chan *input // Inputs to the reader are sent on this channel
quit chan chan<- error
mu sync.Mutex // Protects access to watches, isClosed
watches watchMap // Map of watches (key: i-number)
isClosed bool // Set to true when Close() is first called
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", err)
}
w := &Watcher{
port: port,
watches: make(watchMap),
input: make(chan *input, 1),
Events: make(chan Event, 50),
Errors: make(chan error),
quit: make(chan chan<- error, 1),
}
go w.readEvents()
return w, nil
}
func (w *Watcher) sendEvent(name string, mask uint64) bool {
if mask == 0 {
return false
}
event := w.newEvent(name, uint32(mask))
select {
case ch := <-w.quit:
w.quit <- ch
case w.Events <- event:
}
return true
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
select {
case w.Errors <- err:
return true
case <-w.quit:
}
return false
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
w.isClosed = true
w.mu.Unlock()
// Send "quit" message to the reader goroutine
ch := make(chan error)
w.quit <- ch
if err := w.wakeupReader(); err != nil {
return err
}
return <-ch
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; attempting to watch it more than once will
// return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted.
//
// A path will remain watched if it gets renamed to somewhere else on the same
// filesystem, but the monitor will get removed if the path gets deleted and
// re-created, or if it's moved to a different filesystem.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing
// to the file a temporary file will be written to first, and if successful the
// temporary file is moved to to destination removing the original, or some
// variant thereof. The watcher on the original file is now lost, as it no
// longer exists.
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
func (w *Watcher) Add(name string) error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return errors.New("watcher already closed")
}
w.mu.Unlock()
in := &input{
op: opAddWatch,
path: filepath.Clean(name),
flags: sysFSALLEVENTS,
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error {
in := &input{
op: opRemoveWatch,
path: filepath.Clean(name),
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
// WatchList returns all paths added with [Add] (and are not yet removed).
func (w *Watcher) WatchList() []string {
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.watches))
for _, entry := range w.watches {
for _, watchEntry := range entry {
entries = append(entries, watchEntry.path)
}
}
return entries
}
// These options are from the old golang.org/x/exp/winfsnotify, where you could
// add various options to the watch. This has long since been removed.
//
// The "sys" in the name is misleading as they're not part of any "system".
//
// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
const (
sysFSALLEVENTS = 0xfff
sysFSATTRIB = 0x4
sysFSCREATE = 0x100
sysFSDELETE = 0x200
sysFSDELETESELF = 0x400
sysFSMODIFY = 0x2
sysFSMOVE = 0xc0
sysFSMOVEDFROM = 0x40
sysFSMOVEDTO = 0x80
sysFSMOVESELF = 0x800
sysFSIGNORED = 0x8000
)
func (w *Watcher) newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
e.Op |= Create
}
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF {
e.Op |= Remove
}
if mask&sysFSMODIFY == sysFSMODIFY {
e.Op |= Write
}
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
e.Op |= Rename
}
if mask&sysFSATTRIB == sysFSATTRIB {
e.Op |= Chmod
}
return e
}
const (
opAddWatch = iota
opRemoveWatch
)
const (
provisional uint64 = 1 << (32 + iota)
)
type input struct {
op int
path string
flags uint32
reply chan error
}
type inode struct {
handle windows.Handle
volume uint32
index uint64
}
type watch struct {
ov windows.Overlapped
ino *inode // i-number
path string // Directory path
mask uint64 // Directory itself is being watched with these notify flags
names map[string]uint64 // Map of names being watched and their notify flags
rename string // Remembers the old name while renaming a file
buf [65536]byte // 64K buffer
}
type (
indexMap map[uint64]*watch
watchMap map[uint32]indexMap
)
func (w *Watcher) wakeupReader() error {
err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
if err != nil {
return os.NewSyscallError("PostQueuedCompletionStatus", err)
}
return nil
}
func (w *Watcher) getDir(pathname string) (dir string, err error) {
attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
if err != nil {
return "", os.NewSyscallError("GetFileAttributes", err)
}
if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
dir = pathname
} else {
dir, _ = filepath.Split(pathname)
dir = filepath.Clean(dir)
}
return
}
func (w *Watcher) getIno(path string) (ino *inode, err error) {
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
windows.FILE_LIST_DIRECTORY,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
nil, windows.OPEN_EXISTING,
windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0)
if err != nil {
return nil, os.NewSyscallError("CreateFile", err)
}
var fi windows.ByHandleFileInformation
err = windows.GetFileInformationByHandle(h, &fi)
if err != nil {
windows.CloseHandle(h)
return nil, os.NewSyscallError("GetFileInformationByHandle", err)
}
ino = &inode{
handle: h,
volume: fi.VolumeSerialNumber,
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
}
return ino, nil
}
// Must run within the I/O thread.
func (m watchMap) get(ino *inode) *watch {
if i := m[ino.volume]; i != nil {
return i[ino.index]
}
return nil
}
// Must run within the I/O thread.
func (m watchMap) set(ino *inode, watch *watch) {
i := m[ino.volume]
if i == nil {
i = make(indexMap)
m[ino.volume] = i
}
i[ino.index] = watch
}
// Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64) error {
dir, err := w.getDir(pathname)
if err != nil {
return err
}
ino, err := w.getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watchEntry := w.watches.get(ino)
w.mu.Unlock()
if watchEntry == nil {
_, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0)
if err != nil {
windows.CloseHandle(ino.handle)
return os.NewSyscallError("CreateIoCompletionPort", err)
}
watchEntry = &watch{
ino: ino,
path: dir,
names: make(map[string]uint64),
}
w.mu.Lock()
w.watches.set(ino, watchEntry)
w.mu.Unlock()
flags |= provisional
} else {
windows.CloseHandle(ino.handle)
}
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
err = w.startRead(watchEntry)
if err != nil {
return err
}
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
return nil
}
// Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error {
dir, err := w.getDir(pathname)
if err != nil {
return err
}
ino, err := w.getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watch := w.watches.get(ino)
w.mu.Unlock()
err = windows.CloseHandle(ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
}
if watch == nil {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
}
if pathname == dir {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
watch.mask = 0
} else {
name := filepath.Base(pathname)
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED)
delete(watch.names, name)
}
return w.startRead(watch)
}
// Must run within the I/O thread.
func (w *Watcher) deleteWatch(watch *watch) {
for name, mask := range watch.names {
if mask&provisional == 0 {
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED)
}
delete(watch.names, name)
}
if watch.mask != 0 {
if watch.mask&provisional == 0 {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
}
watch.mask = 0
}
}
// Must run within the I/O thread.
func (w *Watcher) startRead(watch *watch) error {
err := windows.CancelIo(watch.ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CancelIo", err))
w.deleteWatch(watch)
}
mask := w.toWindowsFlags(watch.mask)
for _, m := range watch.names {
mask |= w.toWindowsFlags(m)
}
if mask == 0 {
err := windows.CloseHandle(watch.ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
}
w.mu.Lock()
delete(w.watches[watch.ino.volume], watch.ino.index)
w.mu.Unlock()
return nil
}
rdErr := windows.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0],
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0)
if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
// Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
err = nil
}
w.deleteWatch(watch)
w.startRead(watch)
return err
}
return nil
}
// readEvents reads from the I/O completion port, converts the
// received events into Event objects and sends them via the Events channel.
// Entry point to the I/O thread.
func (w *Watcher) readEvents() {
var (
n uint32
key uintptr
ov *windows.Overlapped
)
runtime.LockOSThread()
for {
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
// This error is handled after the watch == nil check below. NOTE: this
// seems odd, note sure if it's correct.
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
select {
case ch := <-w.quit:
w.mu.Lock()
var indexes []indexMap
for _, index := range w.watches {
indexes = append(indexes, index)
}
w.mu.Unlock()
for _, index := range indexes {
for _, watch := range index {
w.deleteWatch(watch)
w.startRead(watch)
}
}
err := windows.CloseHandle(w.port)
if err != nil {
err = os.NewSyscallError("CloseHandle", err)
}
close(w.Events)
close(w.Errors)
ch <- err
return
case in := <-w.input:
switch in.op {
case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags))
case opRemoveWatch:
in.reply <- w.remWatch(in.path)
}
default:
}
continue
}
switch qErr {
case windows.ERROR_MORE_DATA:
if watch == nil {
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
} else {
// The i/o succeeded but the buffer is full.
// In theory we should be building up a full packet.
// In practice we can get away with just carrying on.
n = uint32(unsafe.Sizeof(watch.buf))
}
case windows.ERROR_ACCESS_DENIED:
// Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
w.deleteWatch(watch)
w.startRead(watch)
continue
case windows.ERROR_OPERATION_ABORTED:
// CancelIo was called on this handle
continue
default:
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
continue
case nil:
}
var offset uint32
for {
if n == 0 {
w.sendError(errors.New("short read in readEvents()"))
break
}
// Point "raw" to the event in the buffer
raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
// Create a buf that is the size of the path name
size := int(raw.FileNameLength / 2)
var buf []uint16
// TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
sh.Data = uintptr(unsafe.Pointer(&raw.FileName))
sh.Len = size
sh.Cap = size
name := windows.UTF16ToString(buf)
fullname := filepath.Join(watch.path, name)
var mask uint64
switch raw.Action {
case windows.FILE_ACTION_REMOVED:
mask = sysFSDELETESELF
case windows.FILE_ACTION_MODIFIED:
mask = sysFSMODIFY
case windows.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case windows.FILE_ACTION_RENAMED_NEW_NAME:
// Update saved path of all sub-watches.
old := filepath.Join(watch.path, watch.rename)
w.mu.Lock()
for _, watchMap := range w.watches {
for _, ww := range watchMap {
if strings.HasPrefix(ww.path, old) {
ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old))
}
}
}
w.mu.Unlock()
if watch.names[watch.rename] != 0 {
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = sysFSMOVESELF
}
}
sendNameEvent := func() {
w.sendEvent(fullname, watch.names[name]&mask)
}
if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
sendNameEvent()
}
if raw.Action == windows.FILE_ACTION_REMOVED {
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED)
delete(watch.names, name)
}
w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action))
if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
fullname = filepath.Join(watch.path, watch.rename)
sendNameEvent()
}
// Move to the next event in the buffer
if raw.NextEntryOffset == 0 {
break
}
offset += raw.NextEntryOffset
// Error!
if offset >= n {
w.sendError(errors.New(
"Windows system assumed buffer larger than it is, events have likely been missed."))
break
}
}
if err := w.startRead(watch); err != nil {
w.sendError(err)
}
}
}
func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
var m uint32
if mask&sysFSMODIFY != 0 {
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
}
if mask&sysFSATTRIB != 0 {
m |= windows.FILE_NOTIFY_CHANGE_ATTRIBUTES
}
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
}
return m
}
func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
switch action {
case windows.FILE_ACTION_ADDED:
return sysFSCREATE
case windows.FILE_ACTION_REMOVED:
return sysFSDELETE
case windows.FILE_ACTION_MODIFIED:
return sysFSMODIFY
case windows.FILE_ACTION_RENAMED_OLD_NAME:
return sysFSMOVEDFROM
case windows.FILE_ACTION_RENAMED_NEW_NAME:
return sysFSMOVEDTO
}
return 0
}

81
vendor/github.com/fsnotify/fsnotify/fsnotify.go generated vendored Normal file
View File

@@ -0,0 +1,81 @@
//go:build !plan9
// +build !plan9
// Package fsnotify provides a cross-platform interface for file system
// notifications.
package fsnotify
import (
"errors"
"fmt"
"strings"
)
// Event represents a file system notification.
type Event struct {
// Path to the file or directory.
//
// Paths are relative to the input; for example with Add("dir") the Name
// will be set to "dir/file" if you create that file, but if you use
// Add("/path/to/dir") it will be "/path/to/dir/file".
Name string
// File operation that triggered the event.
//
// This is a bitmask and some systems may send multiple operations at once.
// Use the Event.Has() method instead of comparing with ==.
Op Op
}
// Op describes a set of file operations.
type Op uint32
// The operations fsnotify can trigger; see the documentation on [Watcher] for a
// full description, and check them with [Event.Has].
const (
Create Op = 1 << iota
Write
Remove
Rename
Chmod
)
// Common errors that can be reported by a watcher
var (
ErrNonExistentWatch = errors.New("can't remove non-existent watcher")
ErrEventOverflow = errors.New("fsnotify queue overflow")
)
func (op Op) String() string {
var b strings.Builder
if op.Has(Create) {
b.WriteString("|CREATE")
}
if op.Has(Remove) {
b.WriteString("|REMOVE")
}
if op.Has(Write) {
b.WriteString("|WRITE")
}
if op.Has(Rename) {
b.WriteString("|RENAME")
}
if op.Has(Chmod) {
b.WriteString("|CHMOD")
}
if b.Len() == 0 {
return "[no events]"
}
return b.String()[1:]
}
// Has reports if this operation has the given operation.
func (o Op) Has(h Op) bool { return o&h == h }
// Has reports if this event has the given operation.
func (e Event) Has(op Op) bool { return e.Op.Has(op) }
// String returns a string representation of the event with their path.
func (e Event) String() string {
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
}

208
vendor/github.com/fsnotify/fsnotify/mkdoc.zsh generated vendored Normal file
View File

@@ -0,0 +1,208 @@
#!/usr/bin/env zsh
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
setopt err_exit no_unset pipefail extended_glob
# Simple script to update the godoc comments on all watchers. Probably took me
# more time to write this than doing it manually, but ah well 🙃
watcher=$(<<EOF
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # macOS notes
//
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
// Settings" until we have a native FSEvents implementation (see [#11]).
//
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
EOF
)
new=$(<<EOF
// NewWatcher creates a new Watcher.
EOF
)
add=$(<<EOF
// Add starts monitoring the path for changes.
//
// A path can only be watched once; attempting to watch it more than once will
// return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted.
//
// A path will remain watched if it gets renamed to somewhere else on the same
// filesystem, but the monitor will get removed if the path gets deleted and
// re-created, or if it's moved to a different filesystem.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing
// to the file a temporary file will be written to first, and if successful the
// temporary file is moved to to destination removing the original, or some
// variant thereof. The watcher on the original file is now lost, as it no
// longer exists.
//
// Instead, watch the parent directory and use Event.Name to filter out files
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
EOF
)
remove=$(<<EOF
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
EOF
)
close=$(<<EOF
// Close removes all watches and closes the events channel.
EOF
)
watchlist=$(<<EOF
// WatchList returns all paths added with [Add] (and are not yet removed).
EOF
)
events=$(<<EOF
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you
// probably want to wait until you've stopped receiving
// them (see the dedup example in cmd/fsnotify).
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows
// it's never sent.
EOF
)
errors=$(<<EOF
// Errors sends any errors.
EOF
)
set-cmt() {
local pat=$1
local cmt=$2
IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go))
for f in $files; do
IFS=':' local fields=($=f)
local file=$fields[1]
local end=$(( $fields[2] - 1 ))
# Find start of comment.
local start=0
IFS=$'\n' local lines=($(head -n$end $file))
for (( i = 1; i <= $#lines; i++ )); do
local line=$lines[-$i]
if ! grep -q '^[[:space:]]*//' <<<$line; then
start=$(( end - (i - 2) ))
break
fi
done
head -n $(( start - 1 )) $file >/tmp/x
print -r -- $cmt >>/tmp/x
tail -n+$(( end + 1 )) $file >>/tmp/x
mv /tmp/x $file
done
}
set-cmt '^type Watcher struct ' $watcher
set-cmt '^func NewWatcher(' $new
set-cmt '^func (w \*Watcher) Add(' $add
set-cmt '^func (w \*Watcher) Remove(' $remove
set-cmt '^func (w \*Watcher) Close(' $close
set-cmt '^func (w \*Watcher) WatchList(' $watchlist
set-cmt '^[[:space:]]*Events *chan Event$' $events
set-cmt '^[[:space:]]*Errors *chan error$' $errors

8
vendor/github.com/fsnotify/fsnotify/system_bsd.go generated vendored Normal file
View File

@@ -0,0 +1,8 @@
//go:build freebsd || openbsd || netbsd || dragonfly
// +build freebsd openbsd netbsd dragonfly
package fsnotify
import "golang.org/x/sys/unix"
const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC

9
vendor/github.com/fsnotify/fsnotify/system_darwin.go generated vendored Normal file
View File

@@ -0,0 +1,9 @@
//go:build darwin
// +build darwin
package fsnotify
import "golang.org/x/sys/unix"
// note: this constant is not defined on BSD
const openMode = unix.O_EVTONLY | unix.O_CLOEXEC

9
vendor/github.com/google/uuid/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,9 @@
language: go
go:
- 1.4.3
- 1.5.3
- tip
script:
- go test -v ./...

10
vendor/github.com/google/uuid/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# How to contribute
We definitely welcome patches and contribution to this project!
### Legal requirements
In order to protect both you and ourselves, you will need to sign the
[Contributor License Agreement](https://cla.developers.google.com/clas).
You may have already signed it for other Google projects.

9
vendor/github.com/google/uuid/CONTRIBUTORS generated vendored Normal file
View File

@@ -0,0 +1,9 @@
Paul Borman <borman@google.com>
bmatsuo
shawnps
theory
jboverfelt
dsymonds
cd1
wallclockbuilder
dansouza

27
vendor/github.com/google/uuid/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2009,2014 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

19
vendor/github.com/google/uuid/README.md generated vendored Normal file
View File

@@ -0,0 +1,19 @@
# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master)
The uuid package generates and inspects UUIDs based on
[RFC 4122](http://tools.ietf.org/html/rfc4122)
and DCE 1.1: Authentication and Security Services.
This package is based on the github.com/pborman/uuid package (previously named
code.google.com/p/go-uuid). It differs from these earlier packages in that
a UUID is a 16 byte array rather than a byte slice. One loss due to this
change is the ability to represent an invalid UUID (vs a NIL UUID).
###### Install
`go get github.com/google/uuid`
###### Documentation
[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid)
Full `go doc` style documentation for the package can be viewed online without
installing this package by using the GoDoc site here:
http://pkg.go.dev/github.com/google/uuid

80
vendor/github.com/google/uuid/dce.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"fmt"
"os"
)
// A Domain represents a Version 2 domain
type Domain byte
// Domain constants for DCE Security (Version 2) UUIDs.
const (
Person = Domain(0)
Group = Domain(1)
Org = Domain(2)
)
// NewDCESecurity returns a DCE Security (Version 2) UUID.
//
// The domain should be one of Person, Group or Org.
// On a POSIX system the id should be the users UID for the Person
// domain and the users GID for the Group. The meaning of id for
// the domain Org or on non-POSIX systems is site defined.
//
// For a given domain/id pair the same token may be returned for up to
// 7 minutes and 10 seconds.
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
uuid, err := NewUUID()
if err == nil {
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
uuid[9] = byte(domain)
binary.BigEndian.PutUint32(uuid[0:], id)
}
return uuid, err
}
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid.
//
// NewDCESecurity(Person, uint32(os.Getuid()))
func NewDCEPerson() (UUID, error) {
return NewDCESecurity(Person, uint32(os.Getuid()))
}
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid.
//
// NewDCESecurity(Group, uint32(os.Getgid()))
func NewDCEGroup() (UUID, error) {
return NewDCESecurity(Group, uint32(os.Getgid()))
}
// Domain returns the domain for a Version 2 UUID. Domains are only defined
// for Version 2 UUIDs.
func (uuid UUID) Domain() Domain {
return Domain(uuid[9])
}
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
// UUIDs.
func (uuid UUID) ID() uint32 {
return binary.BigEndian.Uint32(uuid[0:4])
}
func (d Domain) String() string {
switch d {
case Person:
return "Person"
case Group:
return "Group"
case Org:
return "Org"
}
return fmt.Sprintf("Domain%d", int(d))
}

12
vendor/github.com/google/uuid/doc.go generated vendored Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uuid generates and inspects UUIDs.
//
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
// Services.
//
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
// maps or compared directly.
package uuid

53
vendor/github.com/google/uuid/hash.go generated vendored Normal file
View File

@@ -0,0 +1,53 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"crypto/md5"
"crypto/sha1"
"hash"
)
// Well known namespace IDs and UUIDs
var (
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
Nil UUID // empty UUID, all zeros
)
// NewHash returns a new UUID derived from the hash of space concatenated with
// data generated by h. The hash should be at least 16 byte in length. The
// first 16 bytes of the hash are used to form the UUID. The version of the
// UUID will be the lower 4 bits of version. NewHash is used to implement
// NewMD5 and NewSHA1.
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset()
h.Write(space[:]) //nolint:errcheck
h.Write(data) //nolint:errcheck
s := h.Sum(nil)
var uuid UUID
copy(uuid[:], s)
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
return uuid
}
// NewMD5 returns a new MD5 (Version 3) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(md5.New(), space, data, 3)
func NewMD5(space UUID, data []byte) UUID {
return NewHash(md5.New(), space, data, 3)
}
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(sha1.New(), space, data, 5)
func NewSHA1(space UUID, data []byte) UUID {
return NewHash(sha1.New(), space, data, 5)
}

38
vendor/github.com/google/uuid/marshal.go generated vendored Normal file
View File

@@ -0,0 +1,38 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "fmt"
// MarshalText implements encoding.TextMarshaler.
func (uuid UUID) MarshalText() ([]byte, error) {
var js [36]byte
encodeHex(js[:], uuid)
return js[:], nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
return err
}
*uuid = id
return nil
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (uuid UUID) MarshalBinary() ([]byte, error) {
return uuid[:], nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (uuid *UUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(uuid[:], data)
return nil
}

90
vendor/github.com/google/uuid/node.go generated vendored Normal file
View File

@@ -0,0 +1,90 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"sync"
)
var (
nodeMu sync.Mutex
ifname string // name of interface being used
nodeID [6]byte // hardware for version 1 UUIDs
zeroID [6]byte // nodeID with only 0's
)
// NodeInterface returns the name of the interface from which the NodeID was
// derived. The interface "user" is returned if the NodeID was set by
// SetNodeID.
func NodeInterface() string {
defer nodeMu.Unlock()
nodeMu.Lock()
return ifname
}
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
// If name is "" then the first usable interface found will be used or a random
// Node ID will be generated. If a named interface cannot be found then false
// is returned.
//
// SetNodeInterface never fails when name is "".
func SetNodeInterface(name string) bool {
defer nodeMu.Unlock()
nodeMu.Lock()
return setNodeInterface(name)
}
func setNodeInterface(name string) bool {
iname, addr := getHardwareInterface(name) // null implementation for js
if iname != "" && addr != nil {
ifname = iname
copy(nodeID[:], addr)
return true
}
// We found no interfaces with a valid hardware address. If name
// does not specify a specific interface generate a random Node ID
// (section 4.1.6)
if name == "" {
ifname = "random"
randomBits(nodeID[:])
return true
}
return false
}
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
// if not already set.
func NodeID() []byte {
defer nodeMu.Unlock()
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nid := nodeID
return nid[:]
}
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
// of id are used. If id is less than 6 bytes then false is returned and the
// Node ID is not set.
func SetNodeID(id []byte) bool {
if len(id) < 6 {
return false
}
defer nodeMu.Unlock()
nodeMu.Lock()
copy(nodeID[:], id)
ifname = "user"
return true
}
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte {
var node [6]byte
copy(node[:], uuid[10:])
return node[:]
}

12
vendor/github.com/google/uuid/node_js.go generated vendored Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js
package uuid
// getHardwareInterface returns nil values for the JS version of the code.
// This remvoves the "net" dependency, because it is not used in the browser.
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
func getHardwareInterface(name string) (string, []byte) { return "", nil }

33
vendor/github.com/google/uuid/node_net.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !js
package uuid
import "net"
var interfaces []net.Interface // cached list of interfaces
// getHardwareInterface returns the name and hardware address of interface name.
// If name is "" then the name and hardware address of one of the system's
// interfaces is returned. If no interfaces are found (name does not exist or
// there are no interfaces) then "", nil is returned.
//
// Only addresses of at least 6 bytes are returned.
func getHardwareInterface(name string) (string, []byte) {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil {
return "", nil
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
return ifs.Name, ifs.HardwareAddr
}
}
return "", nil
}

118
vendor/github.com/google/uuid/null.go generated vendored Normal file
View File

@@ -0,0 +1,118 @@
// Copyright 2021 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
)
var jsonNull = []byte("null")
// NullUUID represents a UUID that may be null.
// NullUUID implements the SQL driver.Scanner interface so
// it can be used as a scan destination:
//
// var u uuid.NullUUID
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u)
// ...
// if u.Valid {
// // use u.UUID
// } else {
// // NULL value
// }
//
type NullUUID struct {
UUID UUID
Valid bool // Valid is true if UUID is not NULL
}
// Scan implements the SQL driver.Scanner interface.
func (nu *NullUUID) Scan(value interface{}) error {
if value == nil {
nu.UUID, nu.Valid = Nil, false
return nil
}
err := nu.UUID.Scan(value)
if err != nil {
nu.Valid = false
return err
}
nu.Valid = true
return nil
}
// Value implements the driver Valuer interface.
func (nu NullUUID) Value() (driver.Value, error) {
if !nu.Valid {
return nil, nil
}
// Delegate to UUID Value function
return nu.UUID.Value()
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (nu NullUUID) MarshalBinary() ([]byte, error) {
if nu.Valid {
return nu.UUID[:], nil
}
return []byte(nil), nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (nu *NullUUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(nu.UUID[:], data)
nu.Valid = true
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (nu NullUUID) MarshalText() ([]byte, error) {
if nu.Valid {
return nu.UUID.MarshalText()
}
return jsonNull, nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (nu *NullUUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
nu.Valid = false
return err
}
nu.UUID = id
nu.Valid = true
return nil
}
// MarshalJSON implements json.Marshaler.
func (nu NullUUID) MarshalJSON() ([]byte, error) {
if nu.Valid {
return json.Marshal(nu.UUID)
}
return jsonNull, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (nu *NullUUID) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, jsonNull) {
*nu = NullUUID{}
return nil // valid null UUID
}
err := json.Unmarshal(data, &nu.UUID)
nu.Valid = err == nil
return err
}

59
vendor/github.com/google/uuid/sql.go generated vendored Normal file
View File

@@ -0,0 +1,59 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"database/sql/driver"
"fmt"
)
// Scan implements sql.Scanner so UUIDs can be read from databases transparently.
// Currently, database types that map to string and []byte are supported. Please
// consult database-specific driver documentation for matching types.
func (uuid *UUID) Scan(src interface{}) error {
switch src := src.(type) {
case nil:
return nil
case string:
// if an empty UUID comes from a table, we return a null UUID
if src == "" {
return nil
}
// see Parse for required string format
u, err := Parse(src)
if err != nil {
return fmt.Errorf("Scan: %v", err)
}
*uuid = u
case []byte:
// if an empty UUID comes from a table, we return a null UUID
if len(src) == 0 {
return nil
}
// assumes a simple slice of bytes if 16 bytes
// otherwise attempts to parse
if len(src) != 16 {
return uuid.Scan(string(src))
}
copy((*uuid)[:], src)
default:
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
}
return nil
}
// Value implements sql.Valuer so that UUIDs can be written to databases
// transparently. Currently, UUIDs map to strings. Please consult
// database-specific driver documentation for matching types.
func (uuid UUID) Value() (driver.Value, error) {
return uuid.String(), nil
}

123
vendor/github.com/google/uuid/time.go generated vendored Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"sync"
"time"
)
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
// 1582.
type Time int64
const (
lillian = 2299160 // Julian day of 15 Oct 1582
unix = 2440587 // Julian day of 1 Jan 1970
epoch = unix - lillian // Days between epochs
g1582 = epoch * 86400 // seconds between epochs
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
)
var (
timeMu sync.Mutex
lasttime uint64 // last time we returned
clockSeq uint16 // clock sequence for this run
timeNow = time.Now // for testing
)
// UnixTime converts t the number of seconds and nanoseconds using the Unix
// epoch of 1 Jan 1970.
func (t Time) UnixTime() (sec, nsec int64) {
sec = int64(t - g1582ns100)
nsec = (sec % 10000000) * 100
sec /= 10000000
return sec, nsec
}
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
// clock sequence as well as adjusting the clock sequence as needed. An error
// is returned if the current time cannot be determined.
func GetTime() (Time, uint16, error) {
defer timeMu.Unlock()
timeMu.Lock()
return getTime()
}
func getTime() (Time, uint16, error) {
t := timeNow()
// If we don't have a clock sequence already, set one.
if clockSeq == 0 {
setClockSequence(-1)
}
now := uint64(t.UnixNano()/100) + g1582ns100
// If time has gone backwards with this clock sequence then we
// increment the clock sequence
if now <= lasttime {
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
}
lasttime = now
return Time(now), clockSeq, nil
}
// ClockSequence returns the current clock sequence, generating one if not
// already set. The clock sequence is only used for Version 1 UUIDs.
//
// The uuid package does not use global static storage for the clock sequence or
// the last time a UUID was generated. Unless SetClockSequence is used, a new
// random clock sequence is generated the first time a clock sequence is
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
func ClockSequence() int {
defer timeMu.Unlock()
timeMu.Lock()
return clockSequence()
}
func clockSequence() int {
if clockSeq == 0 {
setClockSequence(-1)
}
return int(clockSeq & 0x3fff)
}
// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
// -1 causes a new sequence to be generated.
func SetClockSequence(seq int) {
defer timeMu.Unlock()
timeMu.Lock()
setClockSequence(seq)
}
func setClockSequence(seq int) {
if seq == -1 {
var b [2]byte
randomBits(b[:]) // clock sequence
seq = int(b[0])<<8 | int(b[1])
}
oldSeq := clockSeq
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
if oldSeq != clockSeq {
lasttime = 0
}
}
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. The time is only defined for version 1 and 2 UUIDs.
func (uuid UUID) Time() Time {
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
return Time(time)
}
// ClockSequence returns the clock sequence encoded in uuid.
// The clock sequence is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) ClockSequence() int {
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
}

43
vendor/github.com/google/uuid/util.go generated vendored Normal file
View File

@@ -0,0 +1,43 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// randomBits completely fills slice b with random data.
func randomBits(b []byte) {
if _, err := io.ReadFull(rander, b); err != nil {
panic(err.Error()) // rand should never fail
}
}
// xvalues returns the value of a byte as a hexadecimal digit or 255.
var xvalues = [256]byte{
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
}
// xtob converts hex characters x1 and x2 into a byte.
func xtob(x1, x2 byte) (byte, bool) {
b1 := xvalues[x1]
b2 := xvalues[x2]
return (b1 << 4) | b2, b1 != 255 && b2 != 255
}

294
vendor/github.com/google/uuid/uuid.go generated vendored Normal file
View File

@@ -0,0 +1,294 @@
// Copyright 2018 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
"sync"
)
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
// 4122.
type UUID [16]byte
// A Version represents a UUID's version.
type Version byte
// A Variant represents a UUID's variant.
type Variant byte
// Constants returned by Variant.
const (
Invalid = Variant(iota) // Invalid UUID
RFC4122 // The variant specified in RFC4122
Reserved // Reserved, NCS backward compatibility.
Microsoft // Reserved, Microsoft Corporation backward compatibility.
Future // Reserved for future definition.
)
const randPoolSize = 16 * 16
var (
rander = rand.Reader // random function
poolEnabled = false
poolMu sync.Mutex
poolPos = randPoolSize // protected with poolMu
pool [randPoolSize]byte // protected with poolMu
)
type invalidLengthError struct{ len int }
func (err invalidLengthError) Error() string {
return fmt.Sprintf("invalid UUID length: %d", err.len)
}
// IsInvalidLengthError is matcher function for custom error invalidLengthError
func IsInvalidLengthError(err error) bool {
_, ok := err.(invalidLengthError)
return ok
}
// Parse decodes s into a UUID or returns an error. Both the standard UUID
// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the
// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex
// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
func Parse(s string) (UUID, error) {
var uuid UUID
switch len(s) {
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36:
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9:
if strings.ToLower(s[:9]) != "urn:uuid:" {
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
}
s = s[9:]
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
case 36 + 2:
s = s[1:]
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
case 32:
var ok bool
for i := range uuid {
uuid[i], ok = xtob(s[i*2], s[i*2+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, invalidLengthError{len(s)}
}
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
v, ok := xtob(s[x], s[x+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
uuid[i] = v
}
return uuid, nil
}
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
func ParseBytes(b []byte) (UUID, error) {
var uuid UUID
switch len(b) {
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) {
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
}
b = b[9:]
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
b = b[1:]
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
var ok bool
for i := 0; i < 32; i += 2 {
uuid[i/2], ok = xtob(b[i], b[i+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, invalidLengthError{len(b)}
}
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
v, ok := xtob(b[x], b[x+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
uuid[i] = v
}
return uuid, nil
}
// MustParse is like Parse but panics if the string cannot be parsed.
// It simplifies safe initialization of global variables holding compiled UUIDs.
func MustParse(s string) UUID {
uuid, err := Parse(s)
if err != nil {
panic(`uuid: Parse(` + s + `): ` + err.Error())
}
return uuid
}
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
// does not have a length of 16. The bytes are copied from the slice.
func FromBytes(b []byte) (uuid UUID, err error) {
err = uuid.UnmarshalBinary(b)
return uuid, err
}
// Must returns uuid if err is nil and panics otherwise.
func Must(uuid UUID, err error) UUID {
if err != nil {
panic(err)
}
return uuid
}
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// , or "" if uuid is invalid.
func (uuid UUID) String() string {
var buf [36]byte
encodeHex(buf[:], uuid)
return string(buf[:])
}
// URN returns the RFC 2141 URN form of uuid,
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
func (uuid UUID) URN() string {
var buf [36 + 9]byte
copy(buf[:], "urn:uuid:")
encodeHex(buf[9:], uuid)
return string(buf[:])
}
func encodeHex(dst []byte, uuid UUID) {
hex.Encode(dst, uuid[:4])
dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-'
hex.Encode(dst[14:18], uuid[6:8])
dst[18] = '-'
hex.Encode(dst[19:23], uuid[8:10])
dst[23] = '-'
hex.Encode(dst[24:], uuid[10:])
}
// Variant returns the variant encoded in uuid.
func (uuid UUID) Variant() Variant {
switch {
case (uuid[8] & 0xc0) == 0x80:
return RFC4122
case (uuid[8] & 0xe0) == 0xc0:
return Microsoft
case (uuid[8] & 0xe0) == 0xe0:
return Future
default:
return Reserved
}
}
// Version returns the version of uuid.
func (uuid UUID) Version() Version {
return Version(uuid[6] >> 4)
}
func (v Version) String() string {
if v > 15 {
return fmt.Sprintf("BAD_VERSION_%d", v)
}
return fmt.Sprintf("VERSION_%d", v)
}
func (v Variant) String() string {
switch v {
case RFC4122:
return "RFC4122"
case Reserved:
return "Reserved"
case Microsoft:
return "Microsoft"
case Future:
return "Future"
case Invalid:
return "Invalid"
}
return fmt.Sprintf("BadVariant%d", int(v))
}
// SetRand sets the random number generator to r, which implements io.Reader.
// If r.Read returns an error when the package requests random data then
// a panic will be issued.
//
// Calling SetRand with nil sets the random number generator to the default
// generator.
func SetRand(r io.Reader) {
if r == nil {
rander = rand.Reader
return
}
rander = r
}
// EnableRandPool enables internal randomness pool used for Random
// (Version 4) UUID generation. The pool contains random bytes read from
// the random number generator on demand in batches. Enabling the pool
// may improve the UUID generation throughput significantly.
//
// Since the pool is stored on the Go heap, this feature may be a bad fit
// for security sensitive applications.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func EnableRandPool() {
poolEnabled = true
}
// DisableRandPool disables the randomness pool if it was previously
// enabled with EnableRandPool.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func DisableRandPool() {
poolEnabled = false
defer poolMu.Unlock()
poolMu.Lock()
poolPos = randPoolSize
}

44
vendor/github.com/google/uuid/version1.go generated vendored Normal file
View File

@@ -0,0 +1,44 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
)
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewUUID returns nil. If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewUUID returns nil and an error.
//
// In most cases, New should be used.
func NewUUID() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
timeLow := uint32(now & 0xffffffff)
timeMid := uint16((now >> 32) & 0xffff)
timeHi := uint16((now >> 48) & 0x0fff)
timeHi |= 0x1000 // Version 1
binary.BigEndian.PutUint32(uuid[0:], timeLow)
binary.BigEndian.PutUint16(uuid[4:], timeMid)
binary.BigEndian.PutUint16(uuid[6:], timeHi)
binary.BigEndian.PutUint16(uuid[8:], seq)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}

76
vendor/github.com/google/uuid/version4.go generated vendored Normal file
View File

@@ -0,0 +1,76 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "io"
// New creates a new random UUID or panics. New is equivalent to
// the expression
//
// uuid.Must(uuid.NewRandom())
func New() UUID {
return Must(NewRandom())
}
// NewString creates a new random UUID and returns it as a string or panics.
// NewString is equivalent to the expression
//
// uuid.New().String()
func NewString() string {
return Must(NewRandom()).String()
}
// NewRandom returns a Random (Version 4) UUID.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// Uses the randomness pool if it was enabled with EnableRandPool.
//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
// hit by a meteorite is estimated to be one chance in 17 billion, that
// means the probability is about 0.00000000006 (6 × 1011),
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
if !poolEnabled {
return NewRandomFromReader(rander)
}
return newRandomFromPool()
}
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
func NewRandomFromReader(r io.Reader) (UUID, error) {
var uuid UUID
_, err := io.ReadFull(r, uuid[:])
if err != nil {
return Nil, err
}
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
func newRandomFromPool() (UUID, error) {
var uuid UUID
poolMu.Lock()
if poolPos == randPoolSize {
_, err := io.ReadFull(rander, pool[:])
if err != nil {
poolMu.Unlock()
return Nil, err
}
poolPos = 0
}
copy(uuid[:], pool[poolPos:(poolPos+16)])
poolPos += 16
poolMu.Unlock()
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}

View File

@@ -1,5 +1,5 @@
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine //go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine
// +build darwin freebsd openbsd netbsd dragonfly // +build darwin freebsd openbsd netbsd dragonfly hurd
// +build !appengine // +build !appengine
package isatty package isatty

View File

@@ -411,7 +411,7 @@ log.Info().Msg("hello world")
Equivalent of `Lshortfile`: Equivalent of `Lshortfile`:
```go ```go
zerolog.CallerMarshalFunc = func(file string, line int) string { zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
short := file short := file
for i := len(file) - 1; i > 0; i-- { for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' { if file[i] == '/' {

View File

@@ -337,7 +337,7 @@ func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter {
t := "<nil>" t := "<nil>"
switch tt := i.(type) { switch tt := i.(type) {
case string: case string:
ts, err := time.Parse(TimeFieldFormat, tt) ts, err := time.ParseInLocation(TimeFieldFormat, tt, time.Local)
if err != nil { if err != nil {
t = tt t = tt
} else { } else {
@@ -348,15 +348,19 @@ func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter {
if err != nil { if err != nil {
t = tt.String() t = tt.String()
} else { } else {
var sec, nsec int64 = i, 0 var sec, nsec int64
switch TimeFieldFormat { switch TimeFieldFormat {
case TimeFormatUnixMs: case TimeFormatUnixNano:
nsec = int64(time.Duration(i) * time.Millisecond) sec, nsec = 0, i
sec = 0
case TimeFormatUnixMicro: case TimeFormatUnixMicro:
nsec = int64(time.Duration(i) * time.Microsecond) sec, nsec = 0, int64(time.Duration(i)*time.Microsecond)
sec = 0 case TimeFormatUnixMs:
sec, nsec = 0, int64(time.Duration(i)*time.Millisecond)
default:
sec, nsec = i, 0
} }
ts := time.Unix(sec, nsec) ts := time.Unix(sec, nsec)
t = ts.Format(timeFormat) t = ts.Format(timeFormat)
} }
@@ -385,7 +389,7 @@ func consoleDefaultFormatLevel(noColor bool) Formatter {
case LevelPanicValue: case LevelPanicValue:
l = colorize(colorize("PNC", colorRed, noColor), colorBold, noColor) l = colorize(colorize("PNC", colorRed, noColor), colorBold, noColor)
default: default:
l = colorize("???", colorBold, noColor) l = colorize(ll, colorBold, noColor)
} }
} else { } else {
if i == nil { if i == nil {

19
vendor/github.com/rs/zerolog/ctx.go generated vendored
View File

@@ -14,10 +14,15 @@ func init() {
type ctxKey struct{} type ctxKey struct{}
// WithContext returns a copy of ctx with l associated. If an instance of Logger // WithContext returns a copy of ctx with the receiver attached. The Logger
// is already in the context, the context is not updated. // attached to the provided Context (if any) will not be effected. If the
// receiver's log level is Disabled it will only be attached to the returned
// Context if the provided Context has a previously attached Logger. If the
// provided Context has no attached Logger, a Disabled Logger will not be
// attached.
// //
// For instance, to add a field to an existing logger in the context, use this // Note: to modify the existing Logger attached to a Context (instead of
// replacing it in a new Context), use UpdateContext with the following
// notation: // notation:
// //
// ctx := r.Context() // ctx := r.Context()
@@ -25,13 +30,9 @@ type ctxKey struct{}
// l.UpdateContext(func(c Context) Context { // l.UpdateContext(func(c Context) Context {
// return c.Str("bar", "baz") // return c.Str("bar", "baz")
// }) // })
//
func (l Logger) WithContext(ctx context.Context) context.Context { func (l Logger) WithContext(ctx context.Context) context.Context {
if lp, ok := ctx.Value(ctxKey{}).(*Logger); ok { if _, ok := ctx.Value(ctxKey{}).(*Logger); !ok && l.level == Disabled {
if lp == &l {
// Do not store same logger.
return ctx
}
} else if l.level == Disabled {
// Do not store disabled logger. // Do not store disabled logger.
return ctx return ctx
} }

View File

@@ -707,6 +707,11 @@ func (e *Event) TimeDiff(key string, t time.Time, start time.Time) *Event {
return e return e
} }
// Any is a wrapper around Event.Interface.
func (e *Event) Any(key string, i interface{}) *Event {
return e.Interface(key, i)
}
// Interface adds the field key with i marshaled using reflection. // Interface adds the field key with i marshaled using reflection.
func (e *Event) Interface(key string, i interface{}) *Event { func (e *Event) Interface(key string, i interface{}) *Event {
if e == nil { if e == nil {
@@ -719,6 +724,15 @@ func (e *Event) Interface(key string, i interface{}) *Event {
return e return e
} }
// Type adds the field key with val's type using reflection.
func (e *Event) Type(key string, val interface{}) *Event {
if e == nil {
return e
}
e.buf = enc.AppendType(enc.AppendKey(e.buf, key), val)
return e
}
// CallerSkipFrame instructs any future Caller calls to skip the specified number of frames. // CallerSkipFrame instructs any future Caller calls to skip the specified number of frames.
// This includes those added via hooks from the context. // This includes those added via hooks from the context.
func (e *Event) CallerSkipFrame(skip int) *Event { func (e *Event) CallerSkipFrame(skip int) *Event {

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math" "math"
"net" "net"
"reflect"
) )
// AppendNil inserts a 'Nil' object into the dst byte array. // AppendNil inserts a 'Nil' object into the dst byte array.
@@ -438,6 +439,14 @@ func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte {
return AppendEmbeddedJSON(dst, marshaled) return AppendEmbeddedJSON(dst, marshaled)
} }
// AppendType appends the parameter type (as a string) to the input byte slice.
func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
if i == nil {
return e.AppendString(dst, "<nil>")
}
return e.AppendString(dst, reflect.TypeOf(i).String())
}
// AppendIPAddr encodes and inserts an IP Address (IPv4 or IPv6). // AppendIPAddr encodes and inserts an IP Address (IPv4 or IPv6).
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte { func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
dst = append(dst, majorTypeTags|additionalTypeIntUint16) dst = append(dst, majorTypeTags|additionalTypeIntUint16)

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math" "math"
"net" "net"
"reflect"
"strconv" "strconv"
) )
@@ -369,6 +370,14 @@ func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte {
return append(dst, marshaled...) return append(dst, marshaled...)
} }
// AppendType appends the parameter type (as a string) to the input byte slice.
func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
if i == nil {
return e.AppendString(dst, "<nil>")
}
return e.AppendString(dst, reflect.TypeOf(i).String())
}
// AppendObjectData takes in an object that is already in a byte array // AppendObjectData takes in an object that is already in a byte array
// and adds it to the dst. // and adds it to the dst.
func (Encoder) AppendObjectData(dst []byte, o []byte) []byte { func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {

View File

@@ -105,6 +105,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"strconv" "strconv"
"strings"
) )
// Level defines log levels. // Level defines log levels.
@@ -160,7 +161,7 @@ func (l Level) String() string {
// ParseLevel converts a level string into a zerolog Level value. // ParseLevel converts a level string into a zerolog Level value.
// returns an error if the input string does not match known values. // returns an error if the input string does not match known values.
func ParseLevel(levelStr string) (Level, error) { func ParseLevel(levelStr string) (Level, error) {
switch levelStr { switch strings.ToLower(levelStr) {
case LevelFieldMarshalFunc(TraceLevel): case LevelFieldMarshalFunc(TraceLevel):
return TraceLevel, nil return TraceLevel, nil
case LevelFieldMarshalFunc(DebugLevel): case LevelFieldMarshalFunc(DebugLevel):

View File

@@ -176,7 +176,7 @@ The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits,
```go ```go
result.Int() int64 // -9223372036854775808 to 9223372036854775807 result.Int() int64 // -9223372036854775808 to 9223372036854775807
result.Uint() int64 // 0 to 18446744073709551615 result.Uint() uint64 // 0 to 18446744073709551615
``` ```
## Modifiers and path chaining ## Modifiers and path chaining

View File

@@ -1009,8 +1009,8 @@ func parseObjectPath(path string) (r objectPathResult) {
r.piped = true r.piped = true
} else { } else {
r.path = path[i+1:] r.path = path[i+1:]
r.more = true
} }
r.more = true
return return
} else if path[i] == '|' { } else if path[i] == '|' {
r.part = string(epart) r.part = string(epart)

View File

@@ -446,6 +446,8 @@ Extensions
- [goldmark-embed](https://github.com/13rac1/goldmark-embed): Adds support for rendering embeds from YouTube links. - [goldmark-embed](https://github.com/13rac1/goldmark-embed): Adds support for rendering embeds from YouTube links.
- [goldmark-latex](https://github.com/soypat/goldmark-latex): A $\LaTeX$ renderer that can be passed to `goldmark.WithRenderer()`. - [goldmark-latex](https://github.com/soypat/goldmark-latex): A $\LaTeX$ renderer that can be passed to `goldmark.WithRenderer()`.
- [goldmark-fences](https://github.com/stefanfritsch/goldmark-fences): Support for pandoc-style [fenced divs](https://pandoc.org/MANUAL.html#divs-and-spans) in goldmark. - [goldmark-fences](https://github.com/stefanfritsch/goldmark-fences): Support for pandoc-style [fenced divs](https://pandoc.org/MANUAL.html#divs-and-spans) in goldmark.
- [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams.
- [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations.
goldmark internal(for extension developers) goldmark internal(for extension developers)

Some files were not shown because too many files have changed in this diff Show More