diff -Nru chasquid-0.03+git20170716.6867859/chasquid.go chasquid-0.04/chasquid.go --- chasquid-0.03+git20170716.6867859/chasquid.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/chasquid.go 2018-02-10 23:18:31.000000000 +0000 @@ -1,3 +1,9 @@ +// chasquid is an SMTP (email) server. +// +// It aims to be easy to configure and maintain for a small mail server, at +// the expense of flexibility and functionality. +// +// See https://blitiri.com.ar/p/chasquid for more details. package main import ( @@ -15,12 +21,13 @@ "blitiri.com.ar/go/chasquid/internal/config" "blitiri.com.ar/go/chasquid/internal/courier" - "blitiri.com.ar/go/chasquid/internal/log" + "blitiri.com.ar/go/chasquid/internal/dovecot" "blitiri.com.ar/go/chasquid/internal/maillog" "blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/smtpsrv" - "blitiri.com.ar/go/chasquid/internal/systemd" "blitiri.com.ar/go/chasquid/internal/userdb" + "blitiri.com.ar/go/log" + "blitiri.com.ar/go/systemd" "net/http" _ "net/http/pprof" @@ -89,6 +96,10 @@ s.SetAliasesConfig(conf.SuffixSeparators, conf.DropCharacters) + if conf.DovecotAuth { + loadDovecot(s, conf.DovecotUserdbPath, conf.DovecotClientPath) + } + // Load certificates from "certs//{fullchain,privkey}.pem". // The structure matches letsencrypt's, to make it easier for that case. log.Infof("Loading certificates") @@ -222,6 +233,22 @@ } } +func loadDovecot(s *smtpsrv.Server, userdb, client string) { + a := dovecot.Autodetect(userdb, client) + if a == nil { + log.Errorf("Dovecot autodetection failed, no dovecot fallback") + return + } + + if a != nil { + s.SetAuthFallback(a) + log.Infof("Fallback authenticator: %v", a) + if err := a.Check(); err != nil { + log.Errorf("Failed dovecot authenticator check: %v", err) + } + } +} + // Read a directory, which must have at least some entries. func mustReadDir(path string) []os.FileInfo { dirs, err := ioutil.ReadDir(path) diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/dovecot-auth-cli.go chasquid-0.04/cmd/dovecot-auth-cli/dovecot-auth-cli.go --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/dovecot-auth-cli.go 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/dovecot-auth-cli.go 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,36 @@ +// CLI used for testing the dovecot authentication package. +// +// NOT for production use. +package main + +import ( + "fmt" + "os" + + "blitiri.com.ar/go/chasquid/internal/dovecot" +) + +func main() { + a := dovecot.NewAuth(os.Args[1]+"-userdb", os.Args[1]+"-client") + + var ok bool + var err error + + switch os.Args[2] { + case "exists": + ok, err = a.Exists(os.Args[3]) + case "auth": + ok, err = a.Authenticate(os.Args[3], os.Args[4]) + default: + fmt.Printf("unknown subcommand\n") + os.Exit(1) + } + + if ok { + fmt.Printf("yes\n") + return + } + + fmt.Printf("no: %v\n", err) + os.Exit(1) +} diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/.gitignore chasquid-0.04/cmd/dovecot-auth-cli/.gitignore --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/.gitignore 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,2 @@ +*.log +dovecot-auth-cli diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_auth_error.cmy chasquid-0.04/cmd/dovecot-auth-cli/test_auth_error.cmy --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_auth_error.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test_auth_error.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,21 @@ + +client unix_listen .dovecot-client + +c = ./dovecot-auth-cli .dovecot auth username password + +client -> VERSION 1 1 +client -> SPID 12345 +client -> CUID 12345 +client -> COOKIE lovelycookie +client -> MECH PLAIN +client -> MECH LOGIN +client -> DONE + +client <- VERSION 1 1 +client <~ CPID + +client <- AUTH 1 PLAIN service=smtp secured no-penalty nologin resp=dXNlcm5hbWUAdXNlcm5hbWUAcGFzc3dvcmQ= +client -> OTHER + +c <~ no: invalid response +c wait 1 diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_auth_no.cmy chasquid-0.04/cmd/dovecot-auth-cli/test_auth_no.cmy --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_auth_no.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test_auth_no.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,21 @@ + +client unix_listen .dovecot-client + +c = ./dovecot-auth-cli .dovecot auth username password + +client -> VERSION 1 1 +client -> SPID 12345 +client -> CUID 12345 +client -> COOKIE lovelycookie +client -> MECH PLAIN +client -> MECH LOGIN +client -> DONE + +client <- VERSION 1 1 +client <~ CPID + +client <- AUTH 1 PLAIN service=smtp secured no-penalty nologin resp=dXNlcm5hbWUAdXNlcm5hbWUAcGFzc3dvcmQ= +client -> FAIL 1 + +c <- no: +c wait 1 diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_auth_yes.cmy chasquid-0.04/cmd/dovecot-auth-cli/test_auth_yes.cmy --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_auth_yes.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test_auth_yes.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,21 @@ + +client unix_listen .dovecot-client + +c = ./dovecot-auth-cli .dovecot auth username password + +client -> VERSION 1 1 +client -> SPID 12345 +client -> CUID 12345 +client -> COOKIE lovelycookie +client -> MECH PLAIN +client -> MECH LOGIN +client -> DONE + +client <- VERSION 1 1 +client <~ CPID + +client <- AUTH 1 PLAIN service=smtp secured no-penalty nologin resp=dXNlcm5hbWUAdXNlcm5hbWUAcGFzc3dvcmQ= +client -> OK 1 + +c <- yes +c wait 0 diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_exists_notfound.cmy chasquid-0.04/cmd/dovecot-auth-cli/test_exists_notfound.cmy --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_exists_notfound.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test_exists_notfound.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,16 @@ + +userdb unix_listen .dovecot-userdb + +c = ./dovecot-auth-cli .dovecot exists username + +userdb -> VERSION 1 1 +userdb -> SPID 12345 + +userdb <- VERSION 1 1 +userdb <- USER 1 username service=smtp + +userdb -> NOTFOUND 1 + +c wait 1 + +c <- no: diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_exists_yes.cmy chasquid-0.04/cmd/dovecot-auth-cli/test_exists_yes.cmy --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_exists_yes.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test_exists_yes.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,15 @@ + +userdb unix_listen .dovecot-userdb + +c = ./dovecot-auth-cli .dovecot exists username + +userdb -> VERSION 1 1 +userdb -> SPID 12345 + +userdb <- VERSION 1 1 +userdb <- USER 1 username service=smtp + +userdb -> USER 1 username system_groups_user=blah uid=10 gid=10 + +c <- yes +c wait 0 diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_missing_socket.cmy chasquid-0.04/cmd/dovecot-auth-cli/test_missing_socket.cmy --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test_missing_socket.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test_missing_socket.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,8 @@ + +c = ./dovecot-auth-cli .missingsocket exists username +c <~ no: dial unix .missingsocket-userdb +c wait 1 + +c = ./dovecot-auth-cli .missingsocket auth username password +c <~ no: dial unix .missingsocket-client +c wait 1 diff -Nru chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test.sh chasquid-0.04/cmd/dovecot-auth-cli/test.sh --- chasquid-0.03+git20170716.6867859/cmd/dovecot-auth-cli/test.sh 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/dovecot-auth-cli/test.sh 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e +. $(dirname ${0})/../../test/util/lib.sh + +init + +# Build the binary once, so we can use it and launch it in chamuyero scripts. +# Otherwise, we not only spend time rebuilding it over and over, but also "go +# run" masks the exit code, which is something we care about. +go build dovecot-auth-cli.go + +for i in *.cmy; do + if ! chamuyero $i > $i.log 2>&1 ; then + echo "# Test $i failed, log follows" + cat $i.log + exit 1 + fi +done + +success diff -Nru chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/mda-lmtp.go chasquid-0.04/cmd/mda-lmtp/mda-lmtp.go --- chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/mda-lmtp.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/cmd/mda-lmtp/mda-lmtp.go 2018-02-10 23:18:31.000000000 +0000 @@ -84,6 +84,13 @@ tempExit("Could not get hostname: %v", err) } + if *fromwhom == "<>" { + *fromwhom = "" + } + if *recipient == "<>" { + *recipient = "" + } + cmd(tc, 250, "LHLO %s", hostname) cmd(tc, 250, "MAIL FROM:<%s>", *fromwhom) cmd(tc, 250, "RCPT TO:<%s>", *recipient) diff -Nru chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test-email chasquid-0.04/cmd/mda-lmtp/test-email --- chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test-email 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/mda-lmtp/test-email 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,3 @@ +Subject: test + +This is a test. diff -Nru chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_tcp_null.cmy chasquid-0.04/cmd/mda-lmtp/test_tcp_null.cmy --- chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_tcp_null.cmy 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/cmd/mda-lmtp/test_tcp_null.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,32 @@ + +nc tcp_listen localhost:14932 + +mda |= ./mda-lmtp --addr=localhost:14932 -f "<>" -d "<>" < test-email + +nc -> 220 Hola desde expect + +nc <~ LHLO .* +nc -> 250-Bienvenido! +nc -> 250 Contame... + +nc <- MAIL FROM:<> +nc -> 250 Aja + +nc <- RCPT TO:<> +nc -> 250 Aja + +nc <- DATA +nc -> 354 Dale + +nc <- Subject: test +nc <- +nc <- This is a test. +nc <- . + +nc -> 250 Recibido + +nc <- QUIT +nc -> 221 Chauchas + +mda wait 0 + diff -Nru chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_tcp_success.cmy chasquid-0.04/cmd/mda-lmtp/test_tcp_success.cmy --- chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_tcp_success.cmy 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/cmd/mda-lmtp/test_tcp_success.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -1,7 +1,7 @@ nc tcp_listen localhost:14932 -mda |= ./mda-lmtp --addr=localhost:14932 -f from -d to < .data +mda |= ./mda-lmtp --addr=localhost:14932 -f from -d to < test-email nc -> 220 Hola desde expect diff -Nru chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_unix_failure.cmy chasquid-0.04/cmd/mda-lmtp/test_unix_failure.cmy --- chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_unix_failure.cmy 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/cmd/mda-lmtp/test_unix_failure.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -2,7 +2,7 @@ nc unix_listen .test-sock mda = ./mda-lmtp --addr=.test-sock --addr_network=unix \ - -f from -d to < .data + -f from -d to < test-email nc -> 220 Hola desde expect diff -Nru chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_unix_success.cmy chasquid-0.04/cmd/mda-lmtp/test_unix_success.cmy --- chasquid-0.03+git20170716.6867859/cmd/mda-lmtp/test_unix_success.cmy 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/cmd/mda-lmtp/test_unix_success.cmy 2018-02-10 23:18:31.000000000 +0000 @@ -2,7 +2,7 @@ nc unix_listen .test-sock mda |= ./mda-lmtp --addr=.test-sock --addr_network=unix \ - -f from -d to < .data + -f from -d to < test-email nc -> 220 Hola desde expect diff -Nru chasquid-0.03+git20170716.6867859/cmd/smtp-check/smtp-check.go chasquid-0.04/cmd/smtp-check/smtp-check.go --- chasquid-0.03+git20170716.6867859/cmd/smtp-check/smtp-check.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/cmd/smtp-check/smtp-check.go 2018-02-10 23:18:31.000000000 +0000 @@ -8,8 +8,8 @@ "net" "net/smtp" - "blitiri.com.ar/go/chasquid/internal/spf" "blitiri.com.ar/go/chasquid/internal/tlsconst" + "blitiri.com.ar/go/spf" "golang.org/x/net/idna" ) diff -Nru chasquid-0.03+git20170716.6867859/cmd/spf-check/spf-check.go chasquid-0.04/cmd/spf-check/spf-check.go --- chasquid-0.03+git20170716.6867859/cmd/spf-check/spf-check.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/cmd/spf-check/spf-check.go 2018-02-10 23:18:31.000000000 +0000 @@ -8,7 +8,7 @@ "fmt" "net" - "blitiri.com.ar/go/chasquid/internal/spf" + "blitiri.com.ar/go/spf" ) func main() { diff -Nru chasquid-0.03+git20170716.6867859/debian/changelog chasquid-0.04/debian/changelog --- chasquid-0.03+git20170716.6867859/debian/changelog 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/changelog 2018-02-12 11:10:36.000000000 +0000 @@ -1,3 +1,22 @@ +chasquid (0.04-1) unstable; urgency=medium + + [ Alberto Bertogli ] + * New upstream release. + * Ignore dovecot-auth-cli (internal binary not useful for production). + * Add new dependencies for the packages that were split off. + * Standards-Version: 4.1.3 (no adjustments needed). + * Update compat and debhelper (to 11) + * gbp.conf: No longer generate pristine-tar + * debian/rules: Use pkg-info.mk + * debian/control: Use "optional" priority + * debian/postint: Do not use recursive chown + * debian/patches: Add description to 0001-Use_chasquid_user.patch + * debian/rules: Use dh_installsystemd + * debian/rules: UPGRADING.md as upstream changelog + * Add a debian/watch file + + -- Martín Ferrari Mon, 12 Feb 2018 11:10:36 +0000 + chasquid (0.03+git20170716.6867859-1) unstable; urgency=medium * New upstream release. diff -Nru chasquid-0.03+git20170716.6867859/debian/compat chasquid-0.04/debian/compat --- chasquid-0.03+git20170716.6867859/debian/compat 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/compat 2018-02-12 11:10:36.000000000 +0000 @@ -1 +1 @@ -9 +11 diff -Nru chasquid-0.03+git20170716.6867859/debian/control chasquid-0.04/debian/control --- chasquid-0.03+git20170716.6867859/debian/control 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/control 2018-02-12 11:10:36.000000000 +0000 @@ -1,18 +1,21 @@ Source: chasquid Section: mail -Priority: extra +Priority: optional Maintainer: Debian Go Packaging Team Uploaders: Martín Ferrari , Alberto Bertogli -Build-Depends: debhelper (>= 10), +Build-Depends: debhelper (>= 11), dh-golang (>= 1.17~), golang-any, + golang-blitiri-go-log-dev, + golang-blitiri-go-spf-dev, + golang-blitiri-go-systemd-dev, golang-github-docopt-docopt-go-dev, - golang-goprotobuf-dev, golang-golang-x-crypto-dev, golang-golang-x-net-dev, golang-golang-x-text-dev, -Standards-Version: 4.0.0 + golang-goprotobuf-dev, +Standards-Version: 4.1.3 Homepage: https://blitiri.com.ar/p/chasquid Vcs-Browser: https://anonscm.debian.org/cgit/pkg-go/packages/chasquid.git Vcs-Git: https://anonscm.debian.org/git/pkg-go/packages/chasquid.git diff -Nru chasquid-0.03+git20170716.6867859/debian/gbp.conf chasquid-0.04/debian/gbp.conf --- chasquid-0.03+git20170716.6867859/debian/gbp.conf 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/gbp.conf 2018-02-12 11:10:36.000000000 +0000 @@ -1,6 +1,4 @@ [DEFAULT] -pristine-tar = True -pristine-tar-commit = True git-upstream-tag = v%(version)s upstream-tag = upstream/%(version)s diff -Nru chasquid-0.03+git20170716.6867859/debian/patches/0001-Use_chasquid_user.patch chasquid-0.04/debian/patches/0001-Use_chasquid_user.patch --- chasquid-0.03+git20170716.6867859/debian/patches/0001-Use_chasquid_user.patch 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/patches/0001-Use_chasquid_user.patch 2018-02-12 11:10:36.000000000 +0000 @@ -1,3 +1,7 @@ +Description: Adjust documentation to reference the "chasquid" user +Forwarded: not-needed +Author: Martín Ferrari + --- a/etc/chasquid/README +++ b/etc/chasquid/README @@ -22,7 +22,7 @@ diff -Nru chasquid-0.03+git20170716.6867859/debian/postinst chasquid-0.04/debian/postinst --- chasquid-0.03+git20170716.6867859/debian/postinst 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/postinst 2018-02-12 11:10:36.000000000 +0000 @@ -28,7 +28,7 @@ --group --gecos "chasquid mail daemon" chasquid || true fi - chown -R chasquid:chasquid /var/lib/chasquid || true + chown chasquid:chasquid /var/lib/chasquid || true ;; abort-upgrade|abort-remove|abort-deconfigure) diff -Nru chasquid-0.03+git20170716.6867859/debian/rules chasquid-0.04/debian/rules --- chasquid-0.03+git20170716.6867859/debian/rules 2017-07-16 12:43:18.000000000 +0000 +++ chasquid-0.04/debian/rules 2018-02-12 11:10:36.000000000 +0000 @@ -1,22 +1,21 @@ #!/usr/bin/make -f +include /usr/share/dpkg/pkg-info.mk + # - t-03-queue_persistency is an artifact of dh_golang: there's a .go file with # a main in that directory which is used for integration tests, and should # not be included or built as part of the Debian package. -# - spf-check is an internal binary useful for development but not suited for -# production use. -export DH_GOLANG_EXCLUDES := t-03-queue_persistency cmd/spf-check - -DEBVERS ?= $(shell dpkg-parsechangelog -SVersion) -DEBDATE ?= $(shell dpkg-parsechangelog -SDate) -BUILD_DATE := $(shell date --utc --date='$(DEBDATE)' +%s) +# - spf-check and cmd/dovecot-auth-cli are internal binaries useful for +# development but not suited for production use. +export DH_GOLANG_EXCLUDES := t-03-queue_persistency \ + cmd/spf-check cmd/dovecot-auth-cli BUILDFLAGS := -ldflags \ - " -X main.version=$(DEBVERS) \ - -X main.sourceDateTs=$(BUILD_DATE)" + " -X main.version=$(DEB_VERSION) \ + -X main.sourceDateTs=$(SOURCE_DATE_EPOCH)" %: - dh $@ --with=systemd --buildsystem=golang --with=golang + dh $@ --buildsystem=golang --with=golang override_dh_auto_build: dh_auto_build -- $(BUILDFLAGS) @@ -28,10 +27,13 @@ # Enable both the service and the sockets (by default only the service will be # enabled, and that's not enough for our case). # This is based on openssh's package. -override_dh_systemd_enable: - dh_systemd_enable --name chasquid chasquid.service - dh_systemd_enable --name chasquid --no-enable chasquid-smtp.socket - dh_systemd_enable --name chasquid --no-enable \ +override_dh_installsystemd: + dh_installsystemd --name chasquid chasquid.service + dh_installsystemd --name chasquid --no-enable chasquid-smtp.socket + dh_installsystemd --name chasquid --no-enable \ chasquid-submission.socket - dh_systemd_enable --name chasquid --no-enable \ + dh_installsystemd --name chasquid --no-enable \ chasquid-submission_tls.socket + +override_dh_installchangelogs: + dh_installchangelogs UPGRADING.md diff -Nru chasquid-0.03+git20170716.6867859/debian/watch chasquid-0.04/debian/watch --- chasquid-0.03+git20170716.6867859/debian/watch 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/debian/watch 2018-02-12 11:10:36.000000000 +0000 @@ -0,0 +1,3 @@ +version=4 +opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/chasquid-$1\.tar\.gz/ \ + https://github.com/albertito/chasquid/tags .*/v?(\d\S+)\.tar\.gz diff -Nru chasquid-0.03+git20170716.6867859/docs/dovecot.md chasquid-0.04/docs/dovecot.md --- chasquid-0.03+git20170716.6867859/docs/dovecot.md 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/docs/dovecot.md 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,56 @@ + +# Dovecot integration + +As of version 0.04 (2018-02), [chasquid] has _experimental_ integration with +[dovecot] for authenticating users. + +This means that chasquid can ask dovecot to authenticate users, instead/in +addition to having its own per-domain user databases. + +It is experimental because it was added recently, and the semantics and +options are prone to be changed in the future. If you use this feature, please +let the authors know, at chasquid@googlegroups.com. + + +## Configuring dovecot + +The following needs to be added to the Dovecot configuration, usually in +`/etc/dovecot/conf.d/10-master.conf`: + +``` +service auth { + unix_listener auth-chasquid-userdb { + mode = 0660 + user = chasquid + } + unix_listener auth-chasquid-client { + mode = 0660 + user = chasquid + } +} +``` + +If chasquid is running under a different user, adjust the `user = ` lines +accordingly. + +This lets chasquid issue authentication requests to dovecot. + + +## Configuring chasquid + +Add the following line to `/etc/chasquid/chasquid.conf`: + +``` +dovecot_auth: true +``` + +That should be it, because chasquid will "autodetect" the full path to the +dovecot sockets, by looking in the usual places (tested in Debian, Ubuntu, and +CentOS). + +If chasquid can't find them, the paths can be set with the +`dovecot_userdb_path` and `dovecot_client_path` options. + + +[dovecot]: https://dovecot.org +[chasquid]: https://blitiri.com.ar/p/chasquid diff -Nru chasquid-0.03+git20170716.6867859/internal/auth/auth.go chasquid-0.04/internal/auth/auth.go --- chasquid-0.03+git20170716.6867859/internal/auth/auth.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/auth/auth.go 2018-02-10 23:18:31.000000000 +0000 @@ -1,17 +1,130 @@ +// Package auth implements authentication services for chasquid. package auth import ( "bytes" "encoding/base64" + "errors" "fmt" "math/rand" "strings" "time" "blitiri.com.ar/go/chasquid/internal/normalize" - "blitiri.com.ar/go/chasquid/internal/userdb" ) +// Interface for authentication backends. +type Backend interface { + Authenticate(user, password string) (bool, error) + Exists(user string) (bool, error) + Reload() error +} + +// Interface for authentication backends that don't need to emit errors. +// This allows backends to avoid unnecessary complexity, in exchange for a bit +// more here. +// They can be converted to normal Backend using WrapNoErrorBackend (defined +// below). +type NoErrorBackend interface { + Authenticate(user, password string) bool + Exists(user string) bool + Reload() error +} + +type Authenticator struct { + // Registered backends, map of domain (string) -> Backend. + // Backend operations will _not_ include the domain in the username. + backends map[string]Backend + + // Fallback backend, to use when backends[domain] (which may not exist) + // did not yield a positive result. + // Note that this backend gets the user with the domain included, of the + // form "user@domain". + Fallback Backend + + // How long Authenticate calls should last, approximately. + // This will be applied both for successful and unsuccessful attempts. + // We will increase this number by 0-20%. + AuthDuration time.Duration +} + +func NewAuthenticator() *Authenticator { + return &Authenticator{ + backends: map[string]Backend{}, + AuthDuration: 100 * time.Millisecond, + } +} + +func (a *Authenticator) Register(domain string, be Backend) { + a.backends[domain] = be +} + +// Authenticate the user@domain with the given password. +func (a *Authenticator) Authenticate(user, domain, password string) (bool, error) { + // Make sure the call takes a.AuthDuration + 0-20% regardless of the + // outcome, to prevent basic timing attacks. + defer func(start time.Time) { + elapsed := time.Since(start) + delay := a.AuthDuration - elapsed + if delay > 0 { + maxDelta := int64(float64(delay) * 0.2) + delay += time.Duration(rand.Int63n(maxDelta)) + time.Sleep(delay) + } + }(time.Now()) + + if be, ok := a.backends[domain]; ok { + ok, err := be.Authenticate(user, password) + if ok || err != nil { + return ok, err + } + } + + if a.Fallback != nil { + return a.Fallback.Authenticate(user+"@"+domain, password) + } + + return false, nil +} + +func (a *Authenticator) Exists(user, domain string) (bool, error) { + if be, ok := a.backends[domain]; ok { + ok, err := be.Exists(user) + if ok || err != nil { + return ok, err + } + } + + if a.Fallback != nil { + return a.Fallback.Exists(user + "@" + domain) + } + + return false, nil +} + +// Reload the registered backends. +func (a *Authenticator) Reload() error { + msgs := []string{} + + for domain, be := range a.backends { + err := be.Reload() + if err != nil { + msgs = append(msgs, fmt.Sprintf("%q: %v", domain, err)) + } + } + if a.Fallback != nil { + err := a.Fallback.Reload() + if err != nil { + msgs = append(msgs, fmt.Sprintf(": %v", err)) + } + } + + if len(msgs) > 0 { + return errors.New(strings.Join(msgs, " ; ")) + } + return nil +} + // DecodeResponse decodes a plain auth response. // // It must be a a base64-encoded string of the form: @@ -89,27 +202,25 @@ return } -// How long Authenticate calls should last, approximately. -// This will be applied both for successful and unsuccessful attempts. -// We will increase this number by 0-20%. -var AuthenticateTime = 100 * time.Millisecond +// WrapNoErrorBackend wraps a NoErrorBackend, converting it into a valid +// Backend. This is normally used in Auth.Register calls, to register no-error +// backends. +func WrapNoErrorBackend(be NoErrorBackend) Backend { + return &wrapNoErrorBackend{be} +} -// Authenticate user/password on the given database. -func Authenticate(udb *userdb.DB, user, passwd string) bool { - defer func(start time.Time) { - elapsed := time.Since(start) - delay := AuthenticateTime - elapsed - if delay > 0 { - maxDelta := int64(float64(delay) * 0.2) - delay += time.Duration(rand.Int63n(maxDelta)) - time.Sleep(delay) - } - }(time.Now()) +type wrapNoErrorBackend struct { + be NoErrorBackend +} - // Note that the database CAN be nil, to simplify callers. - if udb == nil { - return false - } +func (w *wrapNoErrorBackend) Authenticate(user, password string) (bool, error) { + return w.be.Authenticate(user, password), nil +} + +func (w *wrapNoErrorBackend) Exists(user string) (bool, error) { + return w.be.Exists(user), nil +} - return udb.Authenticate(user, passwd) +func (w *wrapNoErrorBackend) Reload() error { + return w.be.Reload() } diff -Nru chasquid-0.03+git20170716.6867859/internal/auth/auth_test.go chasquid-0.04/internal/auth/auth_test.go --- chasquid-0.03+git20170716.6867859/internal/auth/auth_test.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/auth/auth_test.go 2018-02-10 23:18:31.000000000 +0000 @@ -2,9 +2,11 @@ import ( "encoding/base64" + "fmt" "testing" "time" + "blitiri.com.ar/go/chasquid/internal/dovecot" "blitiri.com.ar/go/chasquid/internal/userdb" ) @@ -58,36 +60,213 @@ db := userdb.New("/dev/null") db.AddUser("user", "password") + a := NewAuthenticator() + a.Register("domain", WrapNoErrorBackend(db)) + + // Shorten the duration to speed up the test. This should still be long + // enough for it to fail if we don't sleep intentionally. + a.AuthDuration = 20 * time.Millisecond + // Test the correct case first + check(t, a, "user", "domain", "password", true) + + // Wrong password, but valid user@domain. ts := time.Now() - if !Authenticate(db, "user", "password") { - t.Errorf("failed valid authentication for user/password") + if ok, _ := a.Authenticate("user", "domain", "invalid"); ok { + t.Errorf("invalid password, but authentication succeeded") } - if time.Since(ts) < AuthenticateTime { - t.Errorf("authentication was too fast") + if time.Since(ts) < a.AuthDuration { + t.Errorf("authentication was too fast (invalid case)") } - // Incorrect cases. - cases := []struct{ user, password string }{ - {"user", "incorrect"}, - {"invalid", "p"}, + // Incorrect cases, where the user@domain do not exist. + cases := []struct{ user, domain, password string }{ + {"user", "unknown", "password"}, + {"invalid", "domain", "p"}, + {"invalid", "unknown", "p"}, + {"user", "", "password"}, + {"invalid", "", "p"}, + {"", "domain", "password"}, + {"", "", ""}, } for _, c := range cases { - ts = time.Now() - if Authenticate(db, c.user, c.password) { - t.Errorf("successful auth on %v", c) - } - if time.Since(ts) < AuthenticateTime { - t.Errorf("authentication was too fast") - } + check(t, a, c.user, c.domain, c.password, false) + } +} + +func check(t *testing.T, a *Authenticator, user, domain, passwd string, expect bool) { + c := fmt.Sprintf("{%s@%s %s}", user, domain, passwd) + ts := time.Now() + + ok, err := a.Authenticate(user, domain, passwd) + if time.Since(ts) < a.AuthDuration { + t.Errorf("auth on %v was too fast", c) + } + if ok != expect { + t.Errorf("auth on %v: got %v, expected %v", c, ok, expect) + } + if err != nil { + t.Errorf("auth on %v: got error %v", c, err) + } + + ok, err = a.Exists(user, domain) + if ok != expect { + t.Errorf("exists on %v: got %v, expected %v", c, ok, expect) + } + if err != nil { + t.Errorf("exists on %v: error %v", c, err) } +} + +func TestInterfaces(t *testing.T) { + var _ NoErrorBackend = userdb.New("/dev/null") + var _ Backend = dovecot.NewAuth("/dev/null", "/dev/null") +} + +// Backend implementation for testing. +type TestBE struct { + users map[string]string + reloadCount int + nextError error +} - // And the special case of a nil userdb. - ts = time.Now() - if Authenticate(nil, "user", "password") { - t.Errorf("successful auth on a nil userdb") +func NewTestBE() *TestBE { + return &TestBE{ + users: map[string]string{}, + } +} +func (d *TestBE) add(user, password string) { + d.users[user] = password +} + +func (d *TestBE) Authenticate(user, password string) (bool, error) { + if d.nextError != nil { + return false, d.nextError + } + + if validP, ok := d.users[user]; ok { + return validP == password, nil + } + return false, nil +} + +func (d *TestBE) Exists(user string) (bool, error) { + if d.nextError != nil { + return false, d.nextError + } + _, ok := d.users[user] + return ok, nil +} + +func (d *TestBE) Reload() error { + d.reloadCount++ + if d.nextError != nil { + return d.nextError + } + return nil +} + +func TestMultipleBackends(t *testing.T) { + domain1 := NewTestBE() + domain2 := NewTestBE() + fallback := NewTestBE() + + a := NewAuthenticator() + a.Register("domain1", domain1) + a.Register("domain2", domain2) + a.Fallback = fallback + + // Shorten the duration to speed up the test. This should still be long + // enough for it to fail if we don't sleep intentionally. + a.AuthDuration = 20 * time.Millisecond + + domain1.add("user1", "passwd1") + domain2.add("user2", "passwd2") + fallback.add("user3@fallback", "passwd3") + fallback.add("user4@domain1", "passwd4") + + // Successful tests. + cases := []struct{ user, domain, password string }{ + {"user1", "domain1", "passwd1"}, + {"user2", "domain2", "passwd2"}, + {"user3", "fallback", "passwd3"}, + {"user4", "domain1", "passwd4"}, + } + for _, c := range cases { + check(t, a, c.user, c.domain, c.password, true) + } + + // Unsuccessful tests (users don't exist). + cases = []struct{ user, domain, password string }{ + {"nobody", "domain1", "p"}, + {"nobody", "domain2", "p"}, + {"nobody", "fallback", "p"}, + {"user3", "", "p"}, + } + for _, c := range cases { + check(t, a, c.user, c.domain, c.password, false) + } +} + +func TestErrors(t *testing.T) { + be := NewTestBE() + be.add("user", "passwd") + + a := NewAuthenticator() + a.Register("domain", be) + a.AuthDuration = 0 + + ok, err := a.Authenticate("user", "domain", "passwd") + if err != nil || !ok { + t.Fatalf("failed auth") + } + + expectedErr := fmt.Errorf("test error") + be.nextError = expectedErr + + ok, err = a.Authenticate("user", "domain", "passwd") + if ok { + t.Errorf("authentication succeeded, expected error") + } + if err != expectedErr { + t.Errorf("expected error, got %v", err) + } + + ok, err = a.Exists("user", "domain") + if ok { + t.Errorf("exists succeeded, expected error") + } + if err != expectedErr { + t.Errorf("expected error, got %v", err) + } +} + +func TestReload(t *testing.T) { + be1 := NewTestBE() + be2 := NewTestBE() + fallback := NewTestBE() + + a := NewAuthenticator() + a.Register("domain1", be1) + a.Register("domain2", be2) + a.Fallback = fallback + + err := a.Reload() + if err != nil { + t.Errorf("unexpected error reloading: %v", err) + } + if be1.reloadCount != 1 || be2.reloadCount != 1 || fallback.reloadCount != 1 { + t.Errorf("unexpected reload counts: %d %d %d != 1 1 1", + be1.reloadCount, be2.reloadCount, fallback.reloadCount) + } + + be2.nextError = fmt.Errorf("test error") + err = a.Reload() + if err == nil { + t.Errorf("expected error reloading, got nil") } - if time.Since(ts) < AuthenticateTime { - t.Errorf("authentication was too fast") + if be1.reloadCount != 2 || be2.reloadCount != 2 || fallback.reloadCount != 2 { + t.Errorf("unexpected reload counts: %d %d %d != 2 2 2", + be1.reloadCount, be2.reloadCount, fallback.reloadCount) } } diff -Nru chasquid-0.03+git20170716.6867859/internal/config/config.go chasquid-0.04/internal/config/config.go --- chasquid-0.03+git20170716.6867859/internal/config/config.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/config/config.go 2018-02-10 23:18:31.000000000 +0000 @@ -8,7 +8,7 @@ "io/ioutil" "os" - "blitiri.com.ar/go/chasquid/internal/log" + "blitiri.com.ar/go/log" "github.com/golang/protobuf/proto" ) @@ -93,4 +93,6 @@ log.Infof(" Suffix separators: %s", c.SuffixSeparators) log.Infof(" Drop characters: %s", c.DropCharacters) log.Infof(" Mail log: %s", c.MailLogPath) + log.Infof(" Dovecot auth: %v (%q, %q)", + c.DovecotAuth, c.DovecotUserdbPath, c.DovecotClientPath) } diff -Nru chasquid-0.03+git20170716.6867859/internal/config/config.pb.go chasquid-0.04/internal/config/config.pb.go --- chasquid-0.03+git20170716.6867859/internal/config/config.pb.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/config/config.pb.go 2018-02-10 23:18:31.000000000 +0000 @@ -90,6 +90,18 @@ // If "", log using the syslog (at MAIL|INFO priority). // Default: MailLogPath string `protobuf:"bytes,12,opt,name=mail_log_path,json=mailLogPath" json:"mail_log_path,omitempty"` + // EXPERIMENTAL - Enable dovecot authentication. + // Domains that don't have an user database will be authenticated via + // dovecot. + DovecotAuth bool `protobuf:"varint,13,opt,name=dovecot_auth,json=dovecotAuth" json:"dovecot_auth,omitempty"` + // EXPERIMENTAL - Dovecot userdb path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-userdb + DovecotUserdbPath string `protobuf:"bytes,14,opt,name=dovecot_userdb_path,json=dovecotUserdbPath" json:"dovecot_userdb_path,omitempty"` + // EXPERIMENTAL - Dovecot client path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-client + DovecotClientPath string `protobuf:"bytes,15,opt,name=dovecot_client_path,json=dovecotClientPath" json:"dovecot_client_path,omitempty"` } func (m *Config) Reset() { *m = Config{} } @@ -104,27 +116,31 @@ func init() { proto.RegisterFile("config.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 351 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x92, 0x4d, 0x4f, 0x22, 0x41, - 0x10, 0x86, 0xc3, 0xc2, 0xf2, 0xd1, 0xc0, 0x2e, 0x74, 0x76, 0xb3, 0xbd, 0x7a, 0x41, 0x2e, 0x90, - 0x18, 0xbd, 0x18, 0xe3, 0xc9, 0x03, 0xc2, 0x51, 0xa3, 0x01, 0xef, 0x9d, 0x1a, 0xa6, 0x99, 0xe9, - 0x64, 0xba, 0x7b, 0x52, 0xd5, 0x10, 0xe4, 0x67, 0xf8, 0x8b, 0xcd, 0x34, 0x32, 0x90, 0xe8, 0xb1, - 0x9e, 0xf7, 0x79, 0x53, 0x35, 0x93, 0x66, 0x9d, 0xa5, 0xb3, 0x2b, 0x9d, 0x5c, 0xe7, 0xe8, 0xbc, - 0x1b, 0xbe, 0xd7, 0x58, 0x7d, 0x1a, 0x00, 0x3f, 0x63, 0xcd, 0xd4, 0x91, 0xb7, 0x60, 0x94, 0xa8, - 0x0c, 0x2a, 0xe3, 0xd6, 0xbc, 0x9c, 0xf9, 0x88, 0xf5, 0x0c, 0x6c, 0x65, 0x0c, 0x1e, 0x24, 0xe9, - 0x9d, 0x92, 0x26, 0x12, 0x3f, 0x06, 0x95, 0x71, 0x75, 0xde, 0x35, 0xb0, 0x9d, 0x81, 0x87, 0x85, - 0xde, 0xa9, 0xa7, 0x88, 0x5f, 0xb0, 0x0e, 0x19, 0x9f, 0x4b, 0x88, 0x63, 0x54, 0x44, 0xa2, 0x3a, - 0xa8, 0x8e, 0x5b, 0xf3, 0x76, 0xc1, 0x26, 0x7b, 0xc4, 0xaf, 0x18, 0xa7, 0x75, 0x64, 0x34, 0x91, - 0x76, 0xb6, 0x14, 0x6b, 0x41, 0xec, 0x1f, 0x93, 0x83, 0x7e, 0xcf, 0xce, 0x4f, 0x74, 0xb7, 0x51, - 0x28, 0x7d, 0x46, 0x65, 0xef, 0x67, 0xe8, 0x89, 0xa3, 0xf2, 0xbc, 0x51, 0xf8, 0x9a, 0xd1, 0xc9, - 0x36, 0xe3, 0xac, 0xf6, 0x0e, 0xb5, 0x4d, 0xca, 0x56, 0x3d, 0x7c, 0x5f, 0xff, 0x98, 0x1c, 0xf4, - 0x5b, 0xf6, 0xcf, 0x80, 0xce, 0x64, 0xac, 0x32, 0xbd, 0x51, 0xf8, 0x26, 0x21, 0x51, 0xd6, 0xcb, - 0x48, 0x5b, 0xd1, 0x08, 0x9d, 0x3f, 0x45, 0x3c, 0xfb, 0x4c, 0x27, 0x45, 0xf8, 0xa0, 0x2d, 0xbf, - 0x63, 0xe2, 0xbb, 0x1a, 0x60, 0x42, 0xa2, 0x19, 0x2e, 0xfc, 0xfb, 0xa5, 0x37, 0xc1, 0x84, 0xf8, - 0x7f, 0xd6, 0x0c, 0x3f, 0x35, 0xd6, 0x28, 0x5a, 0x61, 0x41, 0xa3, 0x98, 0x67, 0x1a, 0xf9, 0x25, - 0xeb, 0xd3, 0x7a, 0xb5, 0xd2, 0x5b, 0x49, 0x2a, 0x07, 0x04, 0xef, 0x90, 0x04, 0x0b, 0x4e, 0x6f, - 0x1f, 0x2c, 0x4a, 0xce, 0x47, 0xec, 0x77, 0x8c, 0x2e, 0x97, 0xcb, 0x14, 0x10, 0x96, 0x5e, 0x21, - 0x89, 0x76, 0x50, 0x7f, 0x15, 0x78, 0x5a, 0x52, 0x3e, 0x64, 0xdd, 0x70, 0x69, 0xe6, 0x12, 0x99, - 0x83, 0x4f, 0x45, 0x27, 0x68, 0xed, 0x02, 0x3e, 0xba, 0xe4, 0x05, 0x7c, 0x1a, 0xd5, 0xc3, 0xdb, - 0xb8, 0xf9, 0x08, 0x00, 0x00, 0xff, 0xff, 0xfb, 0x6f, 0x6c, 0xff, 0x2b, 0x02, 0x00, 0x00, + // 409 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x92, 0x41, 0x8f, 0x12, 0x31, + 0x14, 0xc7, 0x83, 0xb8, 0x2c, 0x14, 0xd8, 0x5d, 0xaa, 0xc6, 0xaa, 0x17, 0xdc, 0xcb, 0x92, 0x18, + 0xf7, 0x62, 0x8c, 0x27, 0x0f, 0x08, 0x47, 0x8d, 0x86, 0xd5, 0x73, 0xf3, 0x66, 0xa6, 0xcc, 0x34, + 0x99, 0x69, 0x27, 0xef, 0x75, 0x08, 0xf2, 0x3d, 0xfc, 0xbe, 0xa6, 0x0f, 0x18, 0x30, 0xee, 0xb1, + 0xff, 0xdf, 0xef, 0xdf, 0x76, 0xde, 0x54, 0x8c, 0x52, 0xef, 0xd6, 0x36, 0xbf, 0xaf, 0xd1, 0x07, + 0x7f, 0xfb, 0xe7, 0x42, 0xf4, 0x16, 0x1c, 0xc8, 0xd7, 0xa2, 0x5f, 0x78, 0x0a, 0x0e, 0x2a, 0xa3, + 0x3a, 0xd3, 0xce, 0x6c, 0xb0, 0x6a, 0xd7, 0xf2, 0x4e, 0xdc, 0x54, 0xb0, 0xd5, 0x19, 0x04, 0xd0, + 0x64, 0x77, 0x46, 0x57, 0x89, 0x7a, 0x32, 0xed, 0xcc, 0xba, 0xab, 0x71, 0x05, 0xdb, 0x25, 0x04, + 0x78, 0xb0, 0x3b, 0xf3, 0x2d, 0x91, 0x6f, 0xc5, 0x88, 0xaa, 0x50, 0x6b, 0xc8, 0x32, 0x34, 0x44, + 0xaa, 0x3b, 0xed, 0xce, 0x06, 0xab, 0x61, 0xcc, 0xe6, 0xfb, 0x48, 0xbe, 0x17, 0x92, 0x9a, 0xa4, + 0xb2, 0x44, 0xd6, 0xbb, 0x56, 0x7c, 0xca, 0xe2, 0xe4, 0x44, 0x8e, 0xfa, 0x67, 0xf1, 0xe6, 0x4c, + 0xf7, 0x1b, 0x83, 0x3a, 0x94, 0xd4, 0xf6, 0x2e, 0xb8, 0xa7, 0x4e, 0xca, 0xf7, 0x8d, 0xc1, 0x9f, + 0x25, 0x9d, 0x9d, 0x56, 0x79, 0x67, 0x83, 0x47, 0xeb, 0xf2, 0xb6, 0xd5, 0xe3, 0xef, 0x9b, 0x9c, + 0xc8, 0x51, 0xff, 0x28, 0x5e, 0x56, 0x60, 0x4b, 0x9d, 0x99, 0xd2, 0x6e, 0x0c, 0xfe, 0xd6, 0x90, + 0x1b, 0x17, 0x74, 0x62, 0x9d, 0xba, 0xe4, 0xce, 0xf3, 0x88, 0x97, 0x07, 0x3a, 0x8f, 0xf0, 0x8b, + 0x75, 0xf2, 0x93, 0x50, 0x8f, 0xd5, 0x00, 0x73, 0x52, 0x7d, 0xbe, 0xe1, 0x8b, 0xff, 0x7a, 0x73, + 0xcc, 0x49, 0xbe, 0x12, 0x7d, 0x1e, 0x6a, 0x66, 0x51, 0x0d, 0xf8, 0x80, 0xcb, 0xb8, 0x5e, 0x5a, + 0x94, 0xef, 0xc4, 0x84, 0x9a, 0xf5, 0xda, 0x6e, 0x35, 0x99, 0x1a, 0x10, 0x82, 0x47, 0x52, 0x82, + 0x9d, 0x9b, 0x3d, 0x78, 0x68, 0x73, 0x79, 0x27, 0xae, 0x33, 0xf4, 0xb5, 0x4e, 0x0b, 0x40, 0x48, + 0x83, 0x41, 0x52, 0x43, 0x56, 0xaf, 0x62, 0xbc, 0x68, 0x53, 0x79, 0x2b, 0xc6, 0x7c, 0xd3, 0xd2, + 0xe7, 0xba, 0x86, 0x50, 0xa8, 0x11, 0x6b, 0xc3, 0x18, 0x7e, 0xf5, 0xf9, 0x0f, 0x08, 0x45, 0xfc, + 0x89, 0x99, 0xdf, 0x98, 0xd4, 0x07, 0x0d, 0x4d, 0x28, 0xd4, 0x78, 0xda, 0x99, 0xf5, 0x57, 0xc3, + 0x43, 0x36, 0x6f, 0x42, 0x21, 0xef, 0xc5, 0xb3, 0xa3, 0xd2, 0x90, 0xc1, 0x2c, 0xd9, 0x6f, 0x76, + 0xb5, 0x9f, 0xeb, 0x01, 0xfd, 0x62, 0xc2, 0x5b, 0x9e, 0xf9, 0x69, 0x69, 0xe3, 0x6c, 0xd8, 0xbf, + 0xfe, 0xc7, 0x5f, 0x30, 0x89, 0x7e, 0xd2, 0xe3, 0xe7, 0xf9, 0xe1, 0x6f, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x37, 0xa3, 0x19, 0x18, 0xae, 0x02, 0x00, 0x00, } diff -Nru chasquid-0.03+git20170716.6867859/internal/config/config.proto chasquid-0.04/internal/config/config.proto --- chasquid-0.03+git20170716.6867859/internal/config/config.proto 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/config/config.proto 2018-02-10 23:18:31.000000000 +0000 @@ -74,5 +74,19 @@ // If "", log using the syslog (at MAIL|INFO priority). // Default: string mail_log_path = 12; -} + // EXPERIMENTAL - Enable dovecot authentication. + // Domains that don't have an user database will be authenticated via + // dovecot. + bool dovecot_auth = 13; + + // EXPERIMENTAL - Dovecot userdb path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-userdb + string dovecot_userdb_path = 14; + + // EXPERIMENTAL - Dovecot client path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-client + string dovecot_client_path = 15; +} diff -Nru chasquid-0.03+git20170716.6867859/internal/dovecot/dovecot.go chasquid-0.04/internal/dovecot/dovecot.go --- chasquid-0.03+git20170716.6867859/internal/dovecot/dovecot.go 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/internal/dovecot/dovecot.go 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,276 @@ +// Package dovecot implements functions to interact with Dovecot's +// authentication service. +// +// In particular, it supports doing user authorization, and checking if a user +// exists. It is a very basic implementation, with only the minimum needed to +// cover chasquid's needs. +// +// https://wiki.dovecot.org/Design/AuthProtocol +// https://wiki.dovecot.org/Services#auth +package dovecot + +import ( + "encoding/base64" + "errors" + "fmt" + "net" + "net/textproto" + "os" + "strings" + "time" + "unicode" +) + +// Default timeout to use. We expect Dovecot to be quite fast, but don't want +// to hang forever if something gets stuck. +const DefaultTimeout = 5 * time.Second + +var ( + ErrUsernameNotSafe = errors.New("username not safe (contains spaces)") +) + +var defaultUserdbPaths = []string{ + "/var/run/dovecot/auth-chasquid-userdb", + "/var/run/dovecot/auth-userdb", +} + +var defaultClientPaths = []string{ + "/var/run/dovecot/auth-chasquid-client", + "/var/run/dovecot/auth-client", +} + +// Auth represents a particular Dovecot auth service to use. +type Auth struct { + userdbAddr string + clientAddr string + + // Timeout for connection and I/O operations (applies on each call). + // Set to DefaultTimeout by NewAuth. + Timeout time.Duration +} + +// NewAuth returns a new connection against Dovecot authentication service. It +// takes the addresses of userdb and client sockets (usually paths as +// configured in dovecot). +func NewAuth(userdb, client string) *Auth { + return &Auth{ + userdbAddr: userdb, + clientAddr: client, + Timeout: DefaultTimeout, + } +} + +func (a *Auth) String() string { + return fmt.Sprintf("DovecotAuth(%q, %q)", a.userdbAddr, a.clientAddr) +} + +// Check to see if this auth is valid (but may not be working). +func (a *Auth) Check() error { + // We intentionally don't connect or complete any handshakes because + // dovecot may not be up yet, even thought it may be configured properly. + // Just check that the addresses are valid sockets. + if !isUnixSocket(a.userdbAddr) { + return fmt.Errorf("userdb is not an unix socket") + } + if !isUnixSocket(a.clientAddr) { + return fmt.Errorf("client is not an unix socket") + } + + return nil +} + +// Does user exist? +func (a *Auth) Exists(user string) (bool, error) { + if !isUsernameSafe(user) { + return false, ErrUsernameNotSafe + } + + conn, err := a.dial("unix", a.userdbAddr) + if err != nil { + return false, err + } + defer conn.Close() + + // Dovecot greets us with version and server pid. + // VERSION\t\t + // SPID\t + err = expect(conn, "VERSION\t1") + if err != nil { + return false, fmt.Errorf("error receiving version: %v", err) + } + err = expect(conn, "SPID\t") + if err != nil { + return false, fmt.Errorf("error receiving SPID: %v", err) + } + + // Send our version, and then the request. + err = write(conn, "VERSION\t1\t1\n") + if err != nil { + return false, err + } + + err = write(conn, fmt.Sprintf("USER\t1\t%s\tservice=smtp\n", user)) + if err != nil { + return false, err + } + + // Get the response, and we're done. + resp, err := conn.ReadLine() + if err != nil { + return false, fmt.Errorf("error receiving response: %v", err) + } else if strings.HasPrefix(resp, "USER\t1\t"+user+"\t") { + return true, nil + } else if strings.HasPrefix(resp, "NOTFOUND\t") { + return false, nil + } + return false, fmt.Errorf("invalid response: %q", resp) +} + +// Is the password valud for the user? +func (a *Auth) Authenticate(user, passwd string) (bool, error) { + if !isUsernameSafe(user) { + return false, ErrUsernameNotSafe + } + + conn, err := a.dial("unix", a.clientAddr) + if err != nil { + return false, err + } + defer conn.Close() + + // Send our version, and then our PID. + err = write(conn, fmt.Sprintf("VERSION\t1\t1\nCPID\t%d\n", os.Getpid())) + if err != nil { + return false, err + } + + // Read the server-side handshake. We don't care about the contents + // really, so just read all lines until we see the DONE. + for { + resp, err := conn.ReadLine() + if err != nil { + return false, fmt.Errorf("error receiving handshake: %v", err) + } + if resp == "DONE" { + break + } + } + + // We only support PLAIN authentication, so construct the request. + // Note we set the "secured" option, with the assumpition that we got the + // password via a secure channel (like TLS). This is always true for + // chasquid by design, and simplifies the API. + // TODO: does dovecot handle utf8 domains well? do we need to encode them + // in IDNA first? + resp := base64.StdEncoding.EncodeToString( + []byte(fmt.Sprintf("%s\x00%s\x00%s", user, user, passwd))) + err = write(conn, fmt.Sprintf( + "AUTH\t1\tPLAIN\tservice=smtp\tsecured\tno-penalty\tnologin\tresp=%s\n", resp)) + if err != nil { + return false, err + } + + // Get the response, and we're done. + resp, err = conn.ReadLine() + if err != nil { + return false, fmt.Errorf("error receiving response: %v", err) + } else if strings.HasPrefix(resp, "OK\t1") { + return true, nil + } else if strings.HasPrefix(resp, "FAIL\t1") { + return false, nil + } + return false, fmt.Errorf("invalid response: %q", resp) +} + +func (a *Auth) Reload() error { + return nil +} + +func (a *Auth) dial(network, addr string) (*textproto.Conn, error) { + nc, err := net.DialTimeout(network, addr, a.Timeout) + if err != nil { + return nil, err + } + + nc.SetDeadline(time.Now().Add(a.Timeout)) + + return textproto.NewConn(nc), nil +} + +func expect(conn *textproto.Conn, prefix string) error { + resp, err := conn.ReadLine() + if err != nil { + return err + } + if !strings.HasPrefix(resp, prefix) { + return fmt.Errorf("got %q", resp) + } + return nil +} + +func write(conn *textproto.Conn, msg string) error { + _, err := fmt.Fprintf(conn.W, msg) + if err != nil { + return err + } + + return conn.W.Flush() +} + +// isUsernameSafe to use in the dovecot protocol? +// Unfotunately dovecot's protocol is not very robust wrt. whitespace, +// so we need to be careful. +func isUsernameSafe(user string) bool { + for _, r := range user { + if unicode.IsSpace(r) { + return false + } + } + return true +} + +// Autodetect where the dovecot authentication paths are, and return an Auth +// instance for them. If any of userdb or client are != "", they will be used +// and not autodetected. +func Autodetect(userdb, client string) *Auth { + // If both are given, no need to autodtect. + if userdb != "" && client != "" { + return NewAuth(userdb, client) + } + + var userdbs, clients []string + if userdb != "" { + userdbs = append(userdbs, userdb) + } + if client != "" { + clients = append(clients, client) + } + + if len(userdbs) == 0 { + userdbs = append(userdbs, defaultUserdbPaths...) + } + + if len(clients) == 0 { + clients = append(clients, defaultClientPaths...) + } + + // Go through each possiblity, return the first auth that works. + for _, u := range userdbs { + for _, c := range clients { + a := NewAuth(u, c) + if a.Check() == nil { + return a + } + } + } + + return nil +} + +func isUnixSocket(path string) bool { + fi, err := os.Stat(path) + if err != nil { + return false + } + return fi.Mode()&os.ModeSocket != 0 +} diff -Nru chasquid-0.03+git20170716.6867859/internal/dovecot/dovecot_test.go chasquid-0.04/internal/dovecot/dovecot_test.go --- chasquid-0.03+git20170716.6867859/internal/dovecot/dovecot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/internal/dovecot/dovecot_test.go 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,111 @@ +package dovecot + +// The dovecot package is mainly tested via integration/external tests using +// the dovecot-auth-cli tool. See cmd/dovecot-auth-cli for more details. +// The tests here are more narrow and only test specific functionality that is +// easier to cover from Go. + +import ( + "net" + "testing" + + "blitiri.com.ar/go/chasquid/internal/testlib" +) + +func TestUsernameNotSafe(t *testing.T) { + a := NewAuth("/tmp/nothing", "/tmp/nothing") + + cases := []string{ + "a b", " ab", "ab ", "a\tb", "a\t", " ", "\t", "\t "} + for _, c := range cases { + ok, err := a.Authenticate(c, "passwd") + if ok || err != ErrUsernameNotSafe { + t.Errorf("Authenticate(%q, _): got %v, %v", c, ok, err) + } + + ok, err = a.Exists(c) + if ok || err != ErrUsernameNotSafe { + t.Errorf("Exists(%q): got %v, %v", c, ok, err) + } + } +} + +func TestAutodetect(t *testing.T) { + // If we give both parameters to autodetect, it should return a new Auth + // using them, even if they're not valid. + a := Autodetect("uDoesNotExist", "cDoesNotExist") + if a == nil { + t.Errorf("Autodetection with two params failed") + } else if *a != *NewAuth("uDoesNotExist", "cDoesNotExist") { + t.Errorf("Autodetection with two params: got %v", a) + } + + // We override the default paths, so we can point the "defaults" to our + // test environment as needed. + defaultUserdbPaths = []string{"/dev/null"} + defaultClientPaths = []string{"/dev/null"} + + // Autodetect failure: no valid sockets on the list. + a = Autodetect("", "") + if a != nil { + t.Errorf("Autodetection worked with only /dev/null, got %v", a) + } + + // Create a temporary directory, and two sockets on it. + dir := testlib.MustTempDir(t) + defer testlib.RemoveIfOk(t, dir) + + userdb := dir + "/userdb" + client := dir + "/client" + + uL := mustListen(t, userdb) + cL := mustListen(t, client) + + defaultUserdbPaths = append(defaultUserdbPaths, userdb) + defaultClientPaths = append(defaultClientPaths, client) + + // Autodetect should work fine against open sockets. + a = Autodetect("", "") + if a == nil { + t.Errorf("Autodetection failed (open sockets)") + } else if a.userdbAddr != userdb || a.clientAddr != client { + t.Errorf("Expected autodetect to pick {%q, %q}, but got {%q, %q}", + userdb, client, a.userdbAddr, a.clientAddr) + } + + // TODO: Close the two sockets, and re-do the test from above: Autodetect + // should work fine against closed sockets. + // To implement this test, we should call SetUnlinkOnClose, but + // unfortunately that is only available in Go >= 1.8. + // We want to support Go 1.7 for a while as it is in Debian stable; once + // Debian stable moves on, we can implement this test easily. + + // Autodetect should pick the suggestions passed as parameters (if + // possible). + defaultUserdbPaths = []string{"/dev/null"} + defaultClientPaths = []string{"/dev/null", client} + a = Autodetect(userdb, "") + if a == nil { + t.Errorf("Autodetection failed (single parameter)") + } else if a.userdbAddr != userdb || a.clientAddr != client { + t.Errorf("Expected autodetect to pick {%q, %q}, but got {%q, %q}", + userdb, client, a.userdbAddr, a.clientAddr) + } + + uL.Close() + cL.Close() +} + +func mustListen(t *testing.T, path string) *net.UnixListener { + addr, err := net.ResolveUnixAddr("unix", path) + if err != nil { + t.Fatalf("failed to resolve unix addr %q: %v", path, err) + } + + l, err := net.ListenUnix("unix", addr) + if err != nil { + t.Fatalf("failed to listen on %q: %v", path, err) + } + + return l +} diff -Nru chasquid-0.03+git20170716.6867859/internal/log/log.go chasquid-0.04/internal/log/log.go --- chasquid-0.03+git20170716.6867859/internal/log/log.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/log/log.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,251 +0,0 @@ -// Package log implements a simple logger. -// -// It implements an API somewhat similar to "github.com/google/glog" with a -// focus towards logging to stderr, which is useful for systemd-based -// environments. -// -// There are command line flags (defined using the flag package) to control -// the behaviour of the default logger. By default, it will write to stderr -// without timestamps; this is suitable for systemd (or equivalent) logging. -package log - -import ( - "flag" - "fmt" - "io" - "log/syslog" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "time" -) - -// Flags that control the default logging. -var ( - vLevel = flag.Int("v", 0, "Verbosity level (1 = debug)") - - logFile = flag.String("logfile", "", - "file to log to (enables logtime)") - - logToSyslog = flag.String("logtosyslog", "", - "log to syslog, with the given tag") - - logTime = flag.Bool("logtime", false, - "include the time when writing the log to stderr") - - alsoLogToStderr = flag.Bool("alsologtostderr", false, - "also log to stderr, in addition to the file") -) - -// Logging levels. -type Level int - -const ( - Fatal = Level(-2) - Error = Level(-1) - Info = Level(0) - Debug = Level(1) -) - -var levelToLetter = map[Level]string{ - Fatal: "☠", - Error: "E", - Info: "_", - Debug: ".", -} - -// A Logger represents a logging object that writes logs to the given writer. -type Logger struct { - Level Level - LogTime bool - - CallerSkip int - - w io.WriteCloser - sync.Mutex -} - -func New(w io.WriteCloser) *Logger { - return &Logger{ - w: w, - CallerSkip: 0, - Level: Info, - LogTime: true, - } -} - -func NewFile(path string) (*Logger, error) { - f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - - l := New(f) - l.LogTime = true - return l, nil -} - -func NewSyslog(priority syslog.Priority, tag string) (*Logger, error) { - w, err := syslog.New(priority, tag) - if err != nil { - return nil, err - } - - l := New(w) - l.LogTime = false - return l, nil -} - -func (l *Logger) Close() { - l.w.Close() -} - -func (l *Logger) V(level Level) bool { - return level <= l.Level -} - -func (l *Logger) Log(level Level, skip int, format string, a ...interface{}) { - if !l.V(level) { - return - } - - // Message. - msg := fmt.Sprintf(format, a...) - - // Caller. - _, file, line, ok := runtime.Caller(1 + l.CallerSkip + skip) - if !ok { - file = "unknown" - } - fl := fmt.Sprintf("%s:%-4d", filepath.Base(file), line) - if len(fl) > 18 { - fl = fl[len(fl)-18:] - } - msg = fmt.Sprintf("%-18s", fl) + " " + msg - - // Level. - letter, ok := levelToLetter[level] - if !ok { - letter = strconv.Itoa(int(level)) - } - msg = letter + " " + msg - - // Time. - if l.LogTime { - msg = time.Now().Format("20060102 15:04:05.000000 ") + msg - } - - if !strings.HasSuffix(msg, "\n") { - msg += "\n" - } - - l.Lock() - l.w.Write([]byte(msg)) - l.Unlock() -} - -func (l *Logger) Debugf(format string, a ...interface{}) { - l.Log(Debug, 1, format, a...) -} - -func (l *Logger) Infof(format string, a ...interface{}) { - l.Log(Info, 1, format, a...) -} - -func (l *Logger) Errorf(format string, a ...interface{}) error { - l.Log(Error, 1, format, a...) - return fmt.Errorf(format, a...) -} - -func (l *Logger) Fatalf(format string, a ...interface{}) { - l.Log(-2, 1, format, a...) - // TODO: Log traceback? - os.Exit(1) -} - -// The default logger, used by the top-level functions below. -var Default = &Logger{ - w: os.Stderr, - CallerSkip: 1, - Level: Info, - LogTime: false, -} - -// Init the default logger, based on the command-line flags. -// Must be called after flag.Parse(). -func Init() { - var err error - - if *logToSyslog != "" { - Default, err = NewSyslog(syslog.LOG_DAEMON|syslog.LOG_INFO, *logToSyslog) - if err != nil { - panic(err) - } - } else if *logFile != "" { - Default, err = NewFile(*logFile) - if err != nil { - panic(err) - } - *logTime = true - } - - if *alsoLogToStderr && Default.w != os.Stderr { - Default.w = multiWriteCloser(Default.w, os.Stderr) - } - - Default.CallerSkip = 1 - Default.Level = Level(*vLevel) - Default.LogTime = *logTime -} - -func V(level Level) bool { - return Default.V(level) -} - -func Log(level Level, skip int, format string, a ...interface{}) { - Default.Log(level, skip, format, a...) -} - -func Debugf(format string, a ...interface{}) { - Default.Debugf(format, a...) -} - -func Infof(format string, a ...interface{}) { - Default.Infof(format, a...) -} - -func Errorf(format string, a ...interface{}) error { - return Default.Errorf(format, a...) -} - -func Fatalf(format string, a ...interface{}) { - Default.Fatalf(format, a...) -} - -// multiWriteCloser creates a WriteCloser that duplicates its writes and -// closes to all the provided writers. -func multiWriteCloser(wc ...io.WriteCloser) io.WriteCloser { - return mwc(wc) -} - -type mwc []io.WriteCloser - -func (m mwc) Write(p []byte) (n int, err error) { - for _, w := range m { - if n, err = w.Write(p); err != nil { - return - } - } - return -} -func (m mwc) Close() error { - for _, w := range m { - if err := w.Close(); err != nil { - return err - } - } - return nil -} diff -Nru chasquid-0.03+git20170716.6867859/internal/log/log_test.go chasquid-0.04/internal/log/log_test.go --- chasquid-0.03+git20170716.6867859/internal/log/log_test.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/log/log_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,109 +0,0 @@ -package log - -import ( - "io/ioutil" - "os" - "regexp" - "testing" -) - -func mustNewFile(t *testing.T) (string, *Logger) { - f, err := ioutil.TempFile("", "log_test-") - if err != nil { - t.Fatalf("failed to create temp file: %v", err) - } - - l, err := NewFile(f.Name()) - if err != nil { - t.Fatalf("failed to open new log file: %v", err) - } - - return f.Name(), l -} - -func checkContentsMatch(t *testing.T, name, path, expected string) { - content, err := ioutil.ReadFile(path) - if err != nil { - panic(err) - } - - got := string(content) - if !regexp.MustCompile(expected).Match(content) { - t.Errorf("%s: regexp %q did not match %q", - name, expected, got) - } -} - -func testLogger(t *testing.T, fname string, l *Logger) { - l.LogTime = false - l.Infof("message %d", 1) - checkContentsMatch(t, "info-no-time", fname, - "^_ log_test.go:.... message 1\n") - - os.Truncate(fname, 0) - l.Infof("message %d\n", 1) - checkContentsMatch(t, "info-with-newline", fname, - "^_ log_test.go:.... message 1\n") - - os.Truncate(fname, 0) - l.LogTime = true - l.Infof("message %d", 1) - checkContentsMatch(t, "info-with-time", fname, - `^\d{8} ..:..:..\.\d{6} _ log_test.go:.... message 1\n`) - - os.Truncate(fname, 0) - l.LogTime = false - l.Errorf("error %d", 1) - checkContentsMatch(t, "error", fname, `^E log_test.go:.... error 1\n`) - - if l.V(Debug) { - t.Fatalf("Debug level enabled by default (level: %v)", l.Level) - } - - os.Truncate(fname, 0) - l.LogTime = false - l.Debugf("debug %d", 1) - checkContentsMatch(t, "debug-no-log", fname, `^$`) - - os.Truncate(fname, 0) - l.Level = Debug - l.Debugf("debug %d", 1) - checkContentsMatch(t, "debug", fname, `^\. log_test.go:.... debug 1\n`) - - if !l.V(Debug) { - t.Errorf("l.Level = Debug, but V(Debug) = false") - } - - os.Truncate(fname, 0) - l.Level = Info - l.Log(Debug, 0, "log debug %d", 1) - l.Log(Info, 0, "log info %d", 1) - checkContentsMatch(t, "log", fname, - `^_ log_test.go:.... log info 1\n`) - - os.Truncate(fname, 0) - l.Level = Info - l.Log(Fatal, 0, "log fatal %d", 1) - checkContentsMatch(t, "log", fname, - `^☠ log_test.go:.... log fatal 1\n`) -} - -func TestBasic(t *testing.T) { - fname, l := mustNewFile(t) - defer l.Close() - defer os.Remove(fname) - - testLogger(t, fname, l) -} - -func TestDefaultFile(t *testing.T) { - fname, l := mustNewFile(t) - l.Close() - defer os.Remove(fname) - - *logFile = fname - - Init() - - testLogger(t, fname, Default) -} diff -Nru chasquid-0.03+git20170716.6867859/internal/maillog/maillog.go chasquid-0.04/internal/maillog/maillog.go --- chasquid-0.03+git20170716.6867859/internal/maillog/maillog.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/maillog/maillog.go 2018-02-10 23:18:31.000000000 +0000 @@ -10,8 +10,8 @@ "sync" "time" - "blitiri.com.ar/go/chasquid/internal/log" "blitiri.com.ar/go/chasquid/internal/trace" + "blitiri.com.ar/go/log" ) // Global event logs. diff -Nru chasquid-0.03+git20170716.6867859/internal/queue/queue.go chasquid-0.04/internal/queue/queue.go --- chasquid-0.03+git20170716.6867859/internal/queue/queue.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/queue/queue.go 2018-02-10 23:18:31.000000000 +0000 @@ -23,11 +23,11 @@ "blitiri.com.ar/go/chasquid/internal/aliases" "blitiri.com.ar/go/chasquid/internal/courier" "blitiri.com.ar/go/chasquid/internal/envelope" - "blitiri.com.ar/go/chasquid/internal/log" "blitiri.com.ar/go/chasquid/internal/maillog" "blitiri.com.ar/go/chasquid/internal/protoio" "blitiri.com.ar/go/chasquid/internal/set" "blitiri.com.ar/go/chasquid/internal/trace" + "blitiri.com.ar/go/log" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/timestamp" diff -Nru chasquid-0.03+git20170716.6867859/internal/smtpsrv/conn.go chasquid-0.04/internal/smtpsrv/conn.go --- chasquid-0.03+git20170716.6867859/internal/smtpsrv/conn.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/smtpsrv/conn.go 2018-02-10 23:18:31.000000000 +0000 @@ -28,10 +28,9 @@ "blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/queue" "blitiri.com.ar/go/chasquid/internal/set" - "blitiri.com.ar/go/chasquid/internal/spf" "blitiri.com.ar/go/chasquid/internal/tlsconst" "blitiri.com.ar/go/chasquid/internal/trace" - "blitiri.com.ar/go/chasquid/internal/userdb" + "blitiri.com.ar/go/spf" ) // Exported variables. @@ -120,9 +119,9 @@ // Are we using TLS? onTLS bool - // User databases, aliases and local domains, taken from the server at + // Authenticator, aliases and local domains, taken from the server at // creation time. - userDBs map[string]*userdb.DB + authr *auth.Authenticator localDomains *set.String aliasesR *aliases.Resolver dinfo *domaininfo.DB @@ -158,6 +157,17 @@ defer c.tr.Finish() c.tr.Debugf("Connected, mode: %s", c.mode) + if tc, ok := c.conn.(*tls.Conn); ok { + // For TLS connections, complete the handshake and get the state, so + // it can be used when we say hello below. + tc.Handshake() + cstate := tc.ConnectionState() + c.tlsConnState = &cstate + if name := c.tlsConnState.ServerName; name != "" { + c.hostname = name + } + } + c.tc.PrintfLine("220 %s ESMTP chasquid", c.hostname) var cmd, params string @@ -886,7 +896,11 @@ return 535, fmt.Sprintf("error decoding AUTH response: %v", err) } - if auth.Authenticate(c.userDBs[domain], user, passwd) { + authOk, err := c.authr.Authenticate(user, domain, passwd) + if err != nil { + c.tr.Errorf("error authenticating %q@%q: %v", user, domain, err) + } + if authOk { c.authUser = user c.authDomain = domain c.completedAuth = true @@ -918,11 +932,11 @@ // look up "user" in our databases if the domain is local, which is what // we want. user, domain := envelope.Split(addr) - udb := c.userDBs[domain] - if udb == nil { - return false + ok, err := c.authr.Exists(user, domain) + if err != nil { + c.tr.Errorf("error checking if user %q exists: %v", addr, err) } - return udb.HasUser(user) + return ok } func (c *Conn) readCommand() (cmd, params string, err error) { diff -Nru chasquid-0.03+git20170716.6867859/internal/smtpsrv/conn_test.go chasquid-0.04/internal/smtpsrv/conn_test.go --- chasquid-0.03+git20170716.6867859/internal/smtpsrv/conn_test.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/smtpsrv/conn_test.go 2018-02-10 23:18:31.000000000 +0000 @@ -4,9 +4,9 @@ "testing" "blitiri.com.ar/go/chasquid/internal/domaininfo" - "blitiri.com.ar/go/chasquid/internal/spf" "blitiri.com.ar/go/chasquid/internal/testlib" "blitiri.com.ar/go/chasquid/internal/trace" + "blitiri.com.ar/go/spf" ) func TestSecLevel(t *testing.T) { diff -Nru chasquid-0.03+git20170716.6867859/internal/smtpsrv/server.go chasquid-0.04/internal/smtpsrv/server.go --- chasquid-0.03+git20170716.6867859/internal/smtpsrv/server.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/smtpsrv/server.go 2018-02-10 23:18:31.000000000 +0000 @@ -9,13 +9,14 @@ "time" "blitiri.com.ar/go/chasquid/internal/aliases" + "blitiri.com.ar/go/chasquid/internal/auth" "blitiri.com.ar/go/chasquid/internal/courier" "blitiri.com.ar/go/chasquid/internal/domaininfo" - "blitiri.com.ar/go/chasquid/internal/log" "blitiri.com.ar/go/chasquid/internal/maillog" "blitiri.com.ar/go/chasquid/internal/queue" "blitiri.com.ar/go/chasquid/internal/set" "blitiri.com.ar/go/chasquid/internal/userdb" + "blitiri.com.ar/go/log" ) type Server struct { @@ -38,7 +39,8 @@ localDomains *set.String // User databases (per domain). - userDBs map[string]*userdb.DB + // Authenticator. + authr *auth.Authenticator // Aliases resolver. aliasesR *aliases.Resolver @@ -67,7 +69,7 @@ connTimeout: 20 * time.Minute, commandTimeout: 1 * time.Minute, localDomains: &set.String{}, - userDBs: map[string]*userdb.DB{}, + authr: auth.NewAuthenticator(), aliasesR: aliases.NewResolver(), } } @@ -95,13 +97,17 @@ } func (s *Server) AddUserDB(domain string, db *userdb.DB) { - s.userDBs[domain] = db + s.authr.Register(domain, auth.WrapNoErrorBackend(db)) } func (s *Server) AddAliasesFile(domain, f string) error { return s.aliasesR.AddAliasesFile(domain, f) } +func (s *Server) SetAuthFallback(be auth.Backend) { + s.authr.Fallback = be +} + func (s *Server) SetAliasesConfig(suffixSep, dropChars string) { s.aliasesR.SuffixSep = suffixSep s.aliasesR.DropChars = dropChars @@ -145,11 +151,9 @@ log.Errorf("Error reloading aliases: %v", err) } - for domain, udb := range s.userDBs { - err = udb.Reload() - if err != nil { - log.Errorf("Error reloading %q user db: %v", domain, err) - } + err = s.authr.Reload() + if err != nil { + log.Errorf("Error reloading authenticators: %v", err) } } } @@ -219,7 +223,7 @@ mode: mode, tlsConfig: s.tlsConfig, onTLS: mode.TLS, - userDBs: s.userDBs, + authr: s.authr, aliasesR: s.aliasesR, localDomains: s.localDomains, dinfo: s.dinfo, diff -Nru chasquid-0.03+git20170716.6867859/internal/spf/spf.go chasquid-0.04/internal/spf/spf.go --- chasquid-0.03+git20170716.6867859/internal/spf/spf.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/spf/spf.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,412 +0,0 @@ -// Package spf implements SPF (Sender Policy Framework) lookup and validation. -// -// Supported: -// - "all". -// - "include". -// - "a". -// - "mx". -// - "ip4". -// - "ip6". -// - "redirect". -// -// Not supported (return Neutral if used): -// - "exists". -// - "exp". -// - Macros. -// -// References: -// https://tools.ietf.org/html/rfc7208 -// https://en.wikipedia.org/wiki/Sender_Policy_Framework -package spf - -import ( - "fmt" - "net" - "regexp" - "strconv" - "strings" -) - -// Functions that we can override for testing purposes. -var ( - lookupTXT = net.LookupTXT - lookupMX = net.LookupMX - lookupIP = net.LookupIP - lookupAddr = net.LookupAddr -) - -// Results and Errors. Note the values have meaning, we use them in headers. -// https://tools.ietf.org/html/rfc7208#section-8 -type Result string - -var ( - // https://tools.ietf.org/html/rfc7208#section-8.1 - // Not able to reach any conclusion. - None = Result("none") - - // https://tools.ietf.org/html/rfc7208#section-8.2 - // No definite assertion (positive or negative). - Neutral = Result("neutral") - - // https://tools.ietf.org/html/rfc7208#section-8.3 - // Client is authorized to inject mail. - Pass = Result("pass") - - // https://tools.ietf.org/html/rfc7208#section-8.4 - // Client is *not* authorized to use the domain - Fail = Result("fail") - - // https://tools.ietf.org/html/rfc7208#section-8.5 - // Not authorized, but unwilling to make a strong policy statement/ - SoftFail = Result("softfail") - - // https://tools.ietf.org/html/rfc7208#section-8.6 - // Transient error while performing the check. - TempError = Result("temperror") - - // https://tools.ietf.org/html/rfc7208#section-8.7 - // Records could not be correctly interpreted. - PermError = Result("permerror") -) - -var QualToResult = map[byte]Result{ - '+': Pass, - '-': Fail, - '~': SoftFail, - '?': Neutral, -} - -// CheckHost function fetches SPF records, parses them, and evaluates them to -// determine whether a particular host is or is not permitted to send mail -// with a given identity. -// Reference: https://tools.ietf.org/html/rfc7208#section-4 -func CheckHost(ip net.IP, domain string) (Result, error) { - r := &resolution{ip, 0, nil} - return r.Check(domain) -} - -type resolution struct { - ip net.IP - count uint - - // Result of doing a reverse lookup for ip (so we only do it once). - ipNames []string -} - -func (r *resolution) Check(domain string) (Result, error) { - // Limit the number of resolutions to 10 - // https://tools.ietf.org/html/rfc7208#section-4.6.4 - if r.count > 10 { - return PermError, fmt.Errorf("lookup limit reached") - } - r.count++ - - txt, err := getDNSRecord(domain) - if err != nil { - if isTemporary(err) { - return TempError, err - } - // Could not resolve the name, it may be missing the record. - // https://tools.ietf.org/html/rfc7208#section-2.6.1 - return None, err - } - - if txt == "" { - // No record => None. - // https://tools.ietf.org/html/rfc7208#section-4.6 - return None, nil - } - - fields := strings.Fields(txt) - - // redirects must be handled after the rest; instead of having two loops, - // we just move them to the end. - var newfields, redirects []string - for _, field := range fields { - if strings.HasPrefix(field, "redirect:") { - redirects = append(redirects, field) - } else { - newfields = append(newfields, field) - } - } - fields = append(newfields, redirects...) - - for _, field := range fields { - if strings.HasPrefix(field, "v=") { - continue - } - if r.count > 10 { - return PermError, fmt.Errorf("lookup limit reached") - } - if strings.Contains(field, "%") { - return Neutral, fmt.Errorf("macros not supported") - } - - // See if we have a qualifier, defaulting to + (pass). - // https://tools.ietf.org/html/rfc7208#section-4.6.2 - result, ok := QualToResult[field[0]] - if ok { - field = field[1:] - } else { - result = Pass - } - - if field == "all" { - // https://tools.ietf.org/html/rfc7208#section-5.1 - return result, fmt.Errorf("matched 'all'") - } else if strings.HasPrefix(field, "include:") { - if ok, res, err := r.includeField(result, field); ok { - return res, err - } - } else if strings.HasPrefix(field, "a") { - if ok, res, err := r.aField(result, field, domain); ok { - return res, err - } - } else if strings.HasPrefix(field, "mx") { - if ok, res, err := r.mxField(result, field, domain); ok { - return res, err - } - } else if strings.HasPrefix(field, "ip4:") || strings.HasPrefix(field, "ip6:") { - if ok, res, err := r.ipField(result, field); ok { - return res, err - } - } else if strings.HasPrefix(field, "ptr") { - if ok, res, err := r.ptrField(result, field, domain); ok { - return res, err - } - } else if strings.HasPrefix(field, "exists") { - return Neutral, fmt.Errorf("'exists' not supported") - } else if strings.HasPrefix(field, "exp=") { - return Neutral, fmt.Errorf("'exp' not supported") - } else if strings.HasPrefix(field, "redirect=") { - // https://tools.ietf.org/html/rfc7208#section-6.1 - result, err := r.Check(field[len("redirect="):]) - if result == None { - result = PermError - } - return result, err - } else { - // http://www.openspf.org/SPF_Record_Syntax - return PermError, fmt.Errorf("unknown field %q", field) - } - } - - // Got to the end of the evaluation without a result => Neutral. - // https://tools.ietf.org/html/rfc7208#section-4.7 - return Neutral, nil -} - -// getDNSRecord gets TXT records from the given domain, and returns the SPF -// (if any). Note that at most one SPF is allowed per a given domain: -// https://tools.ietf.org/html/rfc7208#section-3 -// https://tools.ietf.org/html/rfc7208#section-3.2 -// https://tools.ietf.org/html/rfc7208#section-4.5 -func getDNSRecord(domain string) (string, error) { - txts, err := lookupTXT(domain) - if err != nil { - return "", err - } - - for _, txt := range txts { - if strings.HasPrefix(txt, "v=spf1 ") { - return txt, nil - } - - // An empty record is explicitly allowed: - // https://tools.ietf.org/html/rfc7208#section-4.5 - if txt == "v=spf1" { - return txt, nil - } - } - - return "", nil -} - -func isTemporary(err error) bool { - derr, ok := err.(*net.DNSError) - return ok && derr.Temporary() -} - -// ipField processes an "ip" field. -func (r *resolution) ipField(res Result, field string) (bool, Result, error) { - fip := field[4:] - if strings.Contains(fip, "/") { - _, ipnet, err := net.ParseCIDR(fip) - if err != nil { - return true, PermError, err - } - if ipnet.Contains(r.ip) { - return true, res, fmt.Errorf("matched %v", ipnet) - } - } else { - ip := net.ParseIP(fip) - if ip == nil { - return true, PermError, fmt.Errorf("invalid ipX value") - } - if ip.Equal(r.ip) { - return true, res, fmt.Errorf("matched %v", ip) - } - } - - return false, "", nil -} - -// ptrField processes a "ptr" field. -func (r *resolution) ptrField(res Result, field, domain string) (bool, Result, error) { - // Extract the domain if the field is in the form "ptr:domain" - if len(field) >= 4 { - domain = field[4:] - - } - - if r.ipNames == nil { - r.count++ - n, err := lookupAddr(r.ip.String()) - if err != nil { - // https://tools.ietf.org/html/rfc7208#section-5 - if isTemporary(err) { - return true, TempError, err - } - return false, "", err - } - r.ipNames = n - } - - for _, n := range r.ipNames { - if strings.HasSuffix(n, domain+".") { - return true, res, fmt.Errorf("matched ptr:%s", domain) - } - } - - return false, "", nil -} - -// includeField processes an "include" field. -func (r *resolution) includeField(res Result, field string) (bool, Result, error) { - // https://tools.ietf.org/html/rfc7208#section-5.2 - incdomain := field[len("include:"):] - ir, err := r.Check(incdomain) - switch ir { - case Pass: - return true, res, err - case Fail, SoftFail, Neutral: - return false, ir, err - case TempError: - return true, TempError, err - case PermError, None: - return true, PermError, err - } - - return false, "", fmt.Errorf("This should never be reached") - -} - -func ipMatch(ip, tomatch net.IP, mask int) (bool, error) { - if mask >= 0 { - _, ipnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", tomatch.String(), mask)) - if err != nil { - return false, err - } - if ipnet.Contains(ip) { - return true, fmt.Errorf("%v", ipnet) - } - return false, nil - } else { - if ip.Equal(tomatch) { - return true, fmt.Errorf("%v", tomatch) - } - return false, nil - } -} - -var aRegexp = regexp.MustCompile("a(:([^/]+))?(/(.+))?") -var mxRegexp = regexp.MustCompile("mx(:([^/]+))?(/(.+))?") - -func domainAndMask(re *regexp.Regexp, field, domain string) (string, int, error) { - var err error - mask := -1 - if groups := re.FindStringSubmatch(field); groups != nil { - if groups[2] != "" { - domain = groups[2] - } - if groups[4] != "" { - mask, err = strconv.Atoi(groups[4]) - if err != nil { - return "", -1, fmt.Errorf("error parsing mask") - } - } - } - - return domain, mask, nil -} - -// aField processes an "a" field. -func (r *resolution) aField(res Result, field, domain string) (bool, Result, error) { - // https://tools.ietf.org/html/rfc7208#section-5.3 - domain, mask, err := domainAndMask(aRegexp, field, domain) - if err != nil { - return true, PermError, err - } - - r.count++ - ips, err := lookupIP(domain) - if err != nil { - // https://tools.ietf.org/html/rfc7208#section-5 - if isTemporary(err) { - return true, TempError, err - } - return false, "", err - } - for _, ip := range ips { - ok, err := ipMatch(r.ip, ip, mask) - if ok { - return true, res, fmt.Errorf("matched 'a' (%v)", err) - } else if err != nil { - return true, PermError, err - } - } - - return false, "", nil -} - -// mxField processes an "mx" field. -func (r *resolution) mxField(res Result, field, domain string) (bool, Result, error) { - // https://tools.ietf.org/html/rfc7208#section-5.4 - domain, mask, err := domainAndMask(mxRegexp, field, domain) - if err != nil { - return true, PermError, err - } - - r.count++ - mxs, err := lookupMX(domain) - if err != nil { - // https://tools.ietf.org/html/rfc7208#section-5 - if isTemporary(err) { - return true, TempError, err - } - return false, "", err - } - mxips := []net.IP{} - for _, mx := range mxs { - r.count++ - ips, err := lookupIP(mx.Host) - if err != nil { - // https://tools.ietf.org/html/rfc7208#section-5 - if isTemporary(err) { - return true, TempError, err - } - return false, "", err - } - mxips = append(mxips, ips...) - } - for _, ip := range mxips { - ok, err := ipMatch(r.ip, ip, mask) - if ok { - return true, res, fmt.Errorf("matched 'mx' (%v)", err) - } else if err != nil { - return true, PermError, err - } - } - - return false, "", nil -} diff -Nru chasquid-0.03+git20170716.6867859/internal/spf/spf_test.go chasquid-0.04/internal/spf/spf_test.go --- chasquid-0.03+git20170716.6867859/internal/spf/spf_test.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/spf/spf_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,182 +0,0 @@ -package spf - -import ( - "flag" - "fmt" - "net" - "os" - "testing" -) - -var txtResults = map[string][]string{} -var txtErrors = map[string]error{} - -func LookupTXT(domain string) (txts []string, err error) { - return txtResults[domain], txtErrors[domain] -} - -var mxResults = map[string][]*net.MX{} -var mxErrors = map[string]error{} - -func LookupMX(domain string) (mxs []*net.MX, err error) { - return mxResults[domain], mxErrors[domain] -} - -var ipResults = map[string][]net.IP{} -var ipErrors = map[string]error{} - -func LookupIP(host string) (ips []net.IP, err error) { - return ipResults[host], ipErrors[host] -} - -var addrResults = map[string][]string{} -var addrErrors = map[string]error{} - -func LookupAddr(host string) (addrs []string, err error) { - return addrResults[host], addrErrors[host] -} - -func TestMain(m *testing.M) { - lookupTXT = LookupTXT - lookupMX = LookupMX - lookupIP = LookupIP - lookupAddr = LookupAddr - - flag.Parse() - os.Exit(m.Run()) -} - -var ip1110 = net.ParseIP("1.1.1.0") -var ip1111 = net.ParseIP("1.1.1.1") -var ip6666 = net.ParseIP("2001:db8::68") -var ip6660 = net.ParseIP("2001:db8::0") - -func TestBasic(t *testing.T) { - cases := []struct { - txt string - res Result - }{ - {"", None}, - {"blah", None}, - {"v=spf1", Neutral}, - {"v=spf1 ", Neutral}, - {"v=spf1 -", PermError}, - {"v=spf1 all", Pass}, - {"v=spf1 +all", Pass}, - {"v=spf1 -all ", Fail}, - {"v=spf1 ~all", SoftFail}, - {"v=spf1 ?all", Neutral}, - {"v=spf1 a ~all", SoftFail}, - {"v=spf1 a/24", Neutral}, - {"v=spf1 a:d1110/24", Pass}, - {"v=spf1 a:d1110", Neutral}, - {"v=spf1 a:d1111", Pass}, - {"v=spf1 a:nothing/24", Neutral}, - {"v=spf1 mx", Neutral}, - {"v=spf1 mx/24", Neutral}, - {"v=spf1 mx:a/montoto ~all", PermError}, - {"v=spf1 mx:d1110/24 ~all", Pass}, - {"v=spf1 ip4:1.2.3.4 ~all", SoftFail}, - {"v=spf1 ip6:12 ~all", PermError}, - {"v=spf1 ip4:1.1.1.1 -all", Pass}, - {"v=spf1 ptr -all", Pass}, - {"v=spf1 ptr:d1111 -all", Pass}, - {"v=spf1 ptr:lalala -all", Pass}, - {"v=spf1 blah", PermError}, - } - - ipResults["d1111"] = []net.IP{ip1111} - ipResults["d1110"] = []net.IP{ip1110} - mxResults["d1110"] = []*net.MX{{"d1110", 5}, {"nothing", 10}} - addrResults["1.1.1.1"] = []string{"lalala.", "domain.", "d1111."} - - for _, c := range cases { - txtResults["domain"] = []string{c.txt} - res, err := CheckHost(ip1111, "domain") - if (res == TempError || res == PermError) && (err == nil) { - t.Errorf("%q: expected error, got nil", c.txt) - } - if res != c.res { - t.Errorf("%q: expected %q, got %q", c.txt, c.res, res) - t.Logf("%q: error: %v", c.txt, err) - } - } -} - -func TestIPv6(t *testing.T) { - cases := []struct { - txt string - res Result - }{ - {"v=spf1 all", Pass}, - {"v=spf1 a ~all", SoftFail}, - {"v=spf1 a/24", Neutral}, - {"v=spf1 a:d6660/24", Pass}, - {"v=spf1 a:d6660", Neutral}, - {"v=spf1 a:d6666", Pass}, - {"v=spf1 a:nothing/24", Neutral}, - {"v=spf1 mx:d6660/24 ~all", Pass}, - {"v=spf1 ip6:2001:db8::68 ~all", Pass}, - {"v=spf1 ip6:2001:db8::1/24 ~all", Pass}, - {"v=spf1 ip6:2001:db8::1/100 ~all", Pass}, - {"v=spf1 ptr -all", Pass}, - {"v=spf1 ptr:d6666 -all", Pass}, - {"v=spf1 ptr:sonlas6 -all", Pass}, - } - - ipResults["d6666"] = []net.IP{ip6666} - ipResults["d6660"] = []net.IP{ip6660} - mxResults["d6660"] = []*net.MX{{"d6660", 5}, {"nothing", 10}} - addrResults["2001:db8::68"] = []string{"sonlas6.", "domain.", "d6666."} - - for _, c := range cases { - txtResults["domain"] = []string{c.txt} - res, err := CheckHost(ip6666, "domain") - if (res == TempError || res == PermError) && (err == nil) { - t.Errorf("%q: expected error, got nil", c.txt) - } - if res != c.res { - t.Errorf("%q: expected %q, got %q", c.txt, c.res, res) - t.Logf("%q: error: %v", c.txt, err) - } - } -} - -func TestNotSupported(t *testing.T) { - cases := []string{ - "v=spf1 exists:blah -all", - "v=spf1 exp=blah -all", - "v=spf1 a:%{o} -all", - } - - for _, txt := range cases { - txtResults["domain"] = []string{txt} - res, err := CheckHost(ip1111, "domain") - if res != Neutral { - t.Errorf("%q: expected neutral, got %v", txt, res) - t.Logf("%q: error: %v", txt, err) - } - } -} - -func TestRecursion(t *testing.T) { - txtResults["domain"] = []string{"v=spf1 include:domain ~all"} - - res, err := CheckHost(ip1111, "domain") - if res != PermError { - t.Errorf("expected permerror, got %v (%v)", res, err) - } -} - -func TestNoRecord(t *testing.T) { - txtResults["d1"] = []string{""} - txtResults["d2"] = []string{"loco", "v=spf2"} - txtErrors["nospf"] = fmt.Errorf("no such domain") - - for _, domain := range []string{"d1", "d2", "d3", "nospf"} { - res, err := CheckHost(ip1111, domain) - if res != None { - t.Errorf("expected none, got %v (%v)", res, err) - } - } -} diff -Nru chasquid-0.03+git20170716.6867859/internal/systemd/systemd.go chasquid-0.04/internal/systemd/systemd.go --- chasquid-0.03+git20170716.6867859/internal/systemd/systemd.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/systemd/systemd.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -// Package systemd implements utility functions to interact with systemd. -package systemd - -import ( - "errors" - "fmt" - "net" - "os" - "strconv" - "strings" - "syscall" -) - -var ( - // Error to return when $LISTEN_PID does not refer to us. - ErrPIDMismatch = errors.New("$LISTEN_PID != our PID") - - // First FD for listeners. - // It's 3 by definition, but using a variable simplifies testing. - firstFD = 3 -) - -// Listeners creates a slice net.Listener from the file descriptors passed -// by systemd, via the LISTEN_FDS environment variable. -// See sd_listen_fds(3) and sd_listen_fds_with_names(3) for more details. -func Listeners() (map[string][]net.Listener, error) { - pidStr := os.Getenv("LISTEN_PID") - nfdsStr := os.Getenv("LISTEN_FDS") - fdNamesStr := os.Getenv("LISTEN_FDNAMES") - fdNames := strings.Split(fdNamesStr, ":") - - // Nothing to do if the variables are not set. - if pidStr == "" || nfdsStr == "" { - return nil, nil - } - - pid, err := strconv.Atoi(pidStr) - if err != nil { - return nil, fmt.Errorf( - "error converting $LISTEN_PID=%q: %v", pidStr, err) - } else if pid != os.Getpid() { - return nil, ErrPIDMismatch - } - - nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) - if err != nil { - return nil, fmt.Errorf( - "error reading $LISTEN_FDS=%q: %v", nfdsStr, err) - } - - // We should have as many names as we have descriptors. - // Note that if we have no descriptors, fdNames will be [""] (due to how - // strings.Split works), so we consider that special case. - if nfds > 0 && (fdNamesStr == "" || len(fdNames) != nfds) { - return nil, fmt.Errorf( - "Incorrect LISTEN_FDNAMES, have you set FileDescriptorName?") - } - - listeners := map[string][]net.Listener{} - - for i := 0; i < nfds; i++ { - fd := firstFD + i - // We don't want childs to inherit these file descriptors. - syscall.CloseOnExec(fd) - - name := fdNames[i] - - sysName := fmt.Sprintf("[systemd-fd-%d-%v]", fd, name) - lis, err := net.FileListener(os.NewFile(uintptr(fd), sysName)) - if err != nil { - return nil, fmt.Errorf( - "Error making listener out of fd %d: %v", fd, err) - } - - listeners[name] = append(listeners[name], lis) - } - - // Remove them from the environment, to prevent accidental reuse (by - // us or children processes). - os.Unsetenv("LISTEN_PID") - os.Unsetenv("LISTEN_FDS") - os.Unsetenv("LISTEN_FDNAMES") - - return listeners, nil -} diff -Nru chasquid-0.03+git20170716.6867859/internal/systemd/systemd_test.go chasquid-0.04/internal/systemd/systemd_test.go --- chasquid-0.03+git20170716.6867859/internal/systemd/systemd_test.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/systemd/systemd_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,178 +0,0 @@ -package systemd - -import ( - "math/rand" - "net" - "os" - "strconv" - "strings" - "testing" -) - -func setenv(pid, fds string, names ...string) { - os.Setenv("LISTEN_PID", pid) - os.Setenv("LISTEN_FDS", fds) - os.Setenv("LISTEN_FDNAMES", strings.Join(names, ":")) -} - -func TestEmptyEnvironment(t *testing.T) { - cases := []struct{ pid, fds string }{ - {"", ""}, - {"123", ""}, - {"", "4"}, - } - for _, c := range cases { - setenv(c.pid, c.fds) - - if ls, err := Listeners(); ls != nil || err != nil { - t.Logf("Case: LISTEN_PID=%q LISTEN_FDS=%q", c.pid, c.fds) - t.Errorf("Unexpected result: %v // %v", ls, err) - } - } -} - -func TestBadEnvironment(t *testing.T) { - // Create a listener so we have something to reference. - l := newListener(t) - firstFD = listenerFd(t, l) - - ourPID := strconv.Itoa(os.Getpid()) - cases := []struct { - pid, fds string - names []string - }{ - {"a", "1", []string{"name"}}, // Invalid PID. - {ourPID, "a", []string{"name"}}, // Invalid number of fds. - {"1", "1", []string{"name"}}, // PID != ourselves. - {ourPID, "1", []string{"name1", "name2"}}, // Too many names. - {ourPID, "1", []string{}}, // Not enough names. - } - for _, c := range cases { - setenv(c.pid, c.fds, c.names...) - - if ls, err := Listeners(); err == nil { - t.Logf("Case: LISTEN_PID=%q LISTEN_FDS=%q LISTEN_FDNAMES=%q", c.pid, c.fds, c.names) - t.Errorf("Unexpected result: %v // %v", ls, err) - } - } -} - -func TestWrongPID(t *testing.T) { - // Find a pid != us. 1 should always work in practice. - pid := 1 - for pid == os.Getpid() { - pid = rand.Int() - } - - setenv(strconv.Itoa(pid), "4") - if _, err := Listeners(); err != ErrPIDMismatch { - t.Errorf("Did not fail with PID mismatch: %v", err) - } -} - -func TestNoFDs(t *testing.T) { - setenv(strconv.Itoa(os.Getpid()), "0") - if ls, err := Listeners(); len(ls) != 0 || err != nil { - t.Errorf("Got a non-empty result: %v // %v", ls, err) - } -} - -// newListener creates a TCP listener. -func newListener(t *testing.T) *net.TCPListener { - addr := &net.TCPAddr{ - Port: 0, - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - t.Fatalf("Could not create TCP listener: %v", err) - } - - return l -} - -// listenerFd returns a file descriptor for the listener. -// Note it is a NEW file descriptor, not the original one. -func listenerFd(t *testing.T, l *net.TCPListener) int { - f, err := l.File() - if err != nil { - t.Fatalf("Could not get TCP listener file: %v", err) - } - - return int(f.Fd()) -} - -func sameAddr(a, b net.Addr) bool { - return a.Network() == b.Network() && a.String() == b.String() -} - -func TestOneSocket(t *testing.T) { - l := newListener(t) - firstFD = listenerFd(t, l) - - setenv(strconv.Itoa(os.Getpid()), "1", "name") - - lsMap, err := Listeners() - if err != nil || len(lsMap) != 1 { - t.Fatalf("Got an invalid result: %v // %v", lsMap, err) - } - - ls := lsMap["name"] - - if !sameAddr(ls[0].Addr(), l.Addr()) { - t.Errorf("Listener 0 address mismatch, expected %#v, got %#v", - l.Addr(), ls[0].Addr()) - } - - if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" { - t.Errorf("Failed to reset the environment") - } -} - -func TestManySockets(t *testing.T) { - // Create two contiguous listeners. - // The test environment does not guarantee us that they are contiguous, so - // keep going until they are. - var l0, l1 *net.TCPListener - var f0, f1 int = -1, -3 - - for f0+1 != f1 { - // We have to be careful with the order of these operations, because - // listenerFd will create *new* file descriptors. - l0 = newListener(t) - l1 = newListener(t) - f0 = listenerFd(t, l0) - f1 = listenerFd(t, l1) - t.Logf("Looping for FDs: %d %d", f0, f1) - } - - firstFD = f0 - - setenv(strconv.Itoa(os.Getpid()), "2", "name1", "name2") - - lsMap, err := Listeners() - if err != nil || len(lsMap) != 2 { - t.Fatalf("Got an invalid result: %v // %v", lsMap, err) - } - - ls := []net.Listener{ - lsMap["name1"][0], - lsMap["name2"][0], - } - - if !sameAddr(ls[0].Addr(), l0.Addr()) { - t.Errorf("Listener 0 address mismatch, expected %#v, got %#v", - l0.Addr(), ls[0].Addr()) - } - - if !sameAddr(ls[1].Addr(), l1.Addr()) { - t.Errorf("Listener 1 address mismatch, expected %#v, got %#v", - l1.Addr(), ls[1].Addr()) - } - - if os.Getenv("LISTEN_PID") != "" || - os.Getenv("LISTEN_FDS") != "" || - os.Getenv("LISTEN_FDNAMES") != "" { - t.Errorf("Failed to reset the environment") - } -} diff -Nru chasquid-0.03+git20170716.6867859/internal/trace/trace.go chasquid-0.04/internal/trace/trace.go --- chasquid-0.03+git20170716.6867859/internal/trace/trace.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/trace/trace.go 2018-02-10 23:18:31.000000000 +0000 @@ -5,7 +5,7 @@ "fmt" "strconv" - "blitiri.com.ar/go/chasquid/internal/log" + "blitiri.com.ar/go/log" nettrace "golang.org/x/net/trace" ) diff -Nru chasquid-0.03+git20170716.6867859/internal/userdb/userdb.go chasquid-0.04/internal/userdb/userdb.go --- chasquid-0.03+git20170716.6867859/internal/userdb/userdb.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/userdb/userdb.go 2018-02-10 23:18:31.000000000 +0000 @@ -178,8 +178,8 @@ return present } -// HasUser returns true if the user is present, False otherwise. -func (db *DB) HasUser(name string) bool { +// Exists returns true if the user is present, False otherwise. +func (db *DB) Exists(name string) bool { db.mu.Lock() _, present := db.db.Users[name] db.mu.Unlock() diff -Nru chasquid-0.03+git20170716.6867859/internal/userdb/userdb_test.go chasquid-0.04/internal/userdb/userdb_test.go --- chasquid-0.03+git20170716.6867859/internal/userdb/userdb_test.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/internal/userdb/userdb_test.go 2018-02-10 23:18:31.000000000 +0000 @@ -129,7 +129,7 @@ db = mustLoad(t, fname) for _, name := range []string{"user1", "ñoño"} { - if !db.HasUser(name) { + if !db.Exists(name) { t.Errorf("user %q not in database", name) } if db.db.Users[name].GetScheme() == nil { @@ -294,12 +294,12 @@ } } -func TestHasUser(t *testing.T) { +func TestExists(t *testing.T) { fname := mustCreateDB(t, "") defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) - if db.HasUser("unknown") { + if db.Exists("unknown") { t.Errorf("unknown user exists") } @@ -307,15 +307,15 @@ t.Fatalf("error adding user: %v", err) } - if db.HasUser("unknown") { + if db.Exists("unknown") { t.Errorf("unknown user exists") } - if !db.HasUser("user") { + if !db.Exists("user") { t.Errorf("known user does not exist") } - if !db.HasUser("user") { + if !db.Exists("user") { t.Errorf("known user does not exist") } } diff -Nru chasquid-0.03+git20170716.6867859/Makefile chasquid-0.04/Makefile --- chasquid-0.03+git20170716.6867859/Makefile 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/Makefile 2018-02-10 23:18:31.000000000 +0000 @@ -11,7 +11,7 @@ default: chasquid -all: chasquid chasquid-util smtp-check spf-check mda-lmtp +all: chasquid chasquid-util smtp-check spf-check mda-lmtp dovecot-auth-cli chasquid: @@ -33,11 +33,15 @@ mda-lmtp: go build ${GOFLAGS} ./cmd/mda-lmtp/ +dovecot-auth-cli: + go build ${GOFLAGS} ./cmd/dovecot-auth-cli/ + test: go test ${GOFLAGS} ./... setsid -w ./test/run.sh setsid -w ./cmd/chasquid-util/test.sh setsid -w ./cmd/mda-lmtp/test.sh + setsid -w ./cmd/dovecot-auth-cli/test.sh install-binaries: chasquid chasquid-util smtp-check mda-lmtp @@ -54,4 +58,5 @@ fi -.PHONY: chasquid chasquid-util smtp-check spf-check mda-lmtp test +.PHONY: chasquid test \ + chasquid-util smtp-check spf-check mda-lmtp dovecot-auth-cli diff -Nru chasquid-0.03+git20170716.6867859/README.md chasquid-0.04/README.md --- chasquid-0.03+git20170716.6867859/README.md 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/README.md 2018-02-10 23:18:31.000000000 +0000 @@ -24,14 +24,12 @@ * SPF checking. * Monitoring HTTP server, with exported variables and tracing to help debugging. - +* Using dovecot for authentication (experimental). The following are intentionally *not* implemented: * Custom email routing and transport. * DKIM/DMARC checking (although the post-data hook can be used for it). -* Different backends for domain and user configuration (Dovecot authentication - may be implemented in the future). ## Status diff -Nru chasquid-0.03+git20170716.6867859/test/t-01-simple_local/run.sh chasquid-0.04/test/t-01-simple_local/run.sh --- chasquid-0.03+git20170716.6867859/test/t-01-simple_local/run.sh 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/test/t-01-simple_local/run.sh 2018-02-10 23:18:31.000000000 +0000 @@ -6,7 +6,7 @@ init # This should fail, as it has no certificates. -rm config/certs/testserver/*.pem +rm -f config/certs/testserver/*.pem if chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config; then fail "chasquid should not start without certificates" fi diff -Nru chasquid-0.03+git20170716.6867859/test/t-09-loop/run.sh chasquid-0.04/test/t-09-loop/run.sh --- chasquid-0.03+git20170716.6867859/test/t-09-loop/run.sh 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/test/t-09-loop/run.sh 2018-02-10 23:18:31.000000000 +0000 @@ -34,8 +34,8 @@ # Wait until one of them has noticed and stopped the loop. while sleep 0.1; do - wget -q -O .data-A/vars http://localhost:1099/debug/vars - wget -q -O .data-B/vars http://localhost:2099/debug/vars + wget -q -o /dev/null -O .data-A/vars http://localhost:1099/debug/vars + wget -q -o /dev/null -O .data-B/vars http://localhost:2099/debug/vars if grep -q '"chasquid/smtpIn/loopsDetected": 1,' .data-?/vars; then break fi diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/config/chasquid.conf chasquid-0.04/test/t-11-dovecot/config/chasquid.conf --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/config/chasquid.conf 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/config/chasquid.conf 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,14 @@ +smtp_address: ":1025" +submission_address: ":1587" +submission_over_tls_address: ":1465" +monitoring_address: ":1099" + +mail_delivery_agent_bin: "test-mda" +mail_delivery_agent_args: "%to%" + +data_dir: "../.data" +mail_log_path: "../.logs/mail_log" + +dovecot_auth: true +dovecot_userdb_path: "/tmp/chasquid-dovecot-test/run/auth-userdb" +dovecot_client_path: "/tmp/chasquid-dovecot-test/run/auth-client" diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/config/dovecot.conf.in chasquid-0.04/test/t-11-dovecot/config/dovecot.conf.in --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/config/dovecot.conf.in 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/config/dovecot.conf.in 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,45 @@ +base_dir = $ROOT/run/ +log_path = $ROOT/dovecot.log +ssl = no + +default_internal_user = $USER +default_login_user = $USER + + +passdb { + driver = passwd-file + args = $ROOT/passwd +} + +userdb { + driver = passwd-file + args = $ROOT/passwd +} + +service auth { + unix_listener auth { + mode = 0666 + } +} + +# Dovecot refuses to start without protocols, so we need to give it one. +protocols = imap + +service imap-login { + chroot = + inet_listener imap { + address = 127.0.0.1 + port = 0 + } +} + +service anvil { + chroot = +} + +# Turn on debugging information, to help troubleshooting issues. +auth_verbose = yes +auth_debug = yes +auth_debug_passwords = yes +auth_verbose_passwords = yes +mail_debug = yes diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/config/passwd chasquid-0.04/test/t-11-dovecot/config/passwd --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/config/passwd 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/config/passwd 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1 @@ +user@srv:{plain}password:1000:1000::/home/user diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/content chasquid-0.04/test/t-11-dovecot/content --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/content 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/content 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,4 @@ +Subject: Prueba desde el test + +Crece desde el test el futuro +Crece desde el test diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/hosts chasquid-0.04/test/t-11-dovecot/hosts --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/hosts 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/hosts 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1 @@ +srv localhost diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/msmtprc chasquid-0.04/test/t-11-dovecot/msmtprc --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/msmtprc 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/msmtprc 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,28 @@ +account default + +host srv +port 1587 + +tls on +tls_trust_file config/certs/srv/fullchain.pem + +from user@srv + +auth on +user user@srv +password password + +account smtpport : default +port 1025 + +account subm_tls : default +port 1465 +tls_starttls off + +account baduser : default +user unknownuser@srv +password secretpassword + +account badpasswd : default +user user@srv +password badsecretpassword diff -Nru chasquid-0.03+git20170716.6867859/test/t-11-dovecot/run.sh chasquid-0.04/test/t-11-dovecot/run.sh --- chasquid-0.03+git20170716.6867859/test/t-11-dovecot/run.sh 1970-01-01 00:00:00.000000000 +0000 +++ chasquid-0.04/test/t-11-dovecot/run.sh 2018-02-10 23:18:31.000000000 +0000 @@ -0,0 +1,73 @@ +#!/bin/bash +# +# This test checks that we can use dovecot as an authentication mechanism. +# +# Setup: +# - chasquid listening on :1025. +# - dovecot listening on unix sockets in .dovecot/ + +set -e +. $(dirname ${0})/../util/lib.sh + +init + +if ! dovecot --version > /dev/null; then + skip "dovecot not installed" + exit 0 +fi + +# Create a temporary directory for dovecot to use, and generate the dovecot +# config based on the template. +# Note the lenght of the path must be < 100, because unix sockets have a low +# limitation, so we use a directory in /tmp, which is not ideal, as a +# workaround. +export ROOT="/tmp/chasquid-dovecot-test" +mkdir -p $ROOT $ROOT/run +rm -f $ROOT/dovecot.log + +envsubst < config/dovecot.conf.in > $ROOT/dovecot.conf +cp -f config/passwd $ROOT/passwd + +dovecot -F -c $ROOT/dovecot.conf & + +# Early tests: run dovecot-auth-cli for testing purposes. These fail early if +# there are obvious problems. +OUT=$(dovecot-auth-cli $ROOT/run/auth exists user@srv || true) +if [ "$OUT" != "yes" ]; then + fail "user does not exist: $OUT" +fi + +OUT=$(dovecot-auth-cli $ROOT/run/auth auth user@srv password || true) +if [ "$OUT" != "yes" ]; then + fail "auth failed: $OUT" +fi + + +# Set up chasquid, using dovecot as authentication backend. +generate_certs_for srv + +mkdir -p .logs +chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config & +wait_until_ready 1025 + +# Send an email as user@srv successfully. +run_msmtp user@srv < content +wait_for_file .mail/user@srv +mail_diff content .mail/user@srv + +# Fail to send to nobody@srv (user does not exist). +if run_msmtp nobody@srv < content 2> /dev/null; then + fail "successfuly sent an email to a non-existent user" +fi + +# Fail to send from baduser@srv (user does not exist). +if run_msmtp -a baduser user@srv < content 2> /dev/null; then + fail "successfully sent an email with a bad user" +fi + +# Fail to send with an incorrect password. +if run_msmtp -a badpasswd user@srv < content 2> /dev/null; then + fail "successfully sent an email with a bad password" +fi + +success diff -Nru chasquid-0.03+git20170716.6867859/test/util/generate_cert.go chasquid-0.04/test/util/generate_cert.go --- chasquid-0.03+git20170716.6867859/test/util/generate_cert.go 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/test/util/generate_cert.go 2018-02-10 23:18:31.000000000 +0000 @@ -25,6 +25,8 @@ "os" "strings" "time" + + "golang.org/x/net/idna" ) var ( @@ -128,7 +130,13 @@ if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { - template.DNSNames = append(template.DNSNames, h) + // We use IDNA-encoded DNS names, otherwise the TLS library won't + // load the certificates. + ih, err := idna.ToASCII(h) + if err != nil { + log.Fatalf("host %q cannot be IDNA-encoded: %v", h, err) + } + template.DNSNames = append(template.DNSNames, ih) } } diff -Nru chasquid-0.03+git20170716.6867859/test/util/lib.sh chasquid-0.04/test/util/lib.sh --- chasquid-0.03+git20170716.6867859/test/util/lib.sh 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/test/util/lib.sh 2018-02-10 23:18:31.000000000 +0000 @@ -49,6 +49,10 @@ >> .add_user_logs } +function dovecot-auth-cli() { + go run ${TBASE}/../../cmd/dovecot-auth-cli/dovecot-auth-cli.go "$@" +} + function run_msmtp() { # msmtp will check that the rc file is only user readable. chmod 600 msmtprc diff -Nru chasquid-0.03+git20170716.6867859/.travis.yml chasquid-0.04/.travis.yml --- chasquid-0.03+git20170716.6867859/.travis.yml 2017-07-16 12:24:40.000000000 +0000 +++ chasquid-0.04/.travis.yml 2018-02-10 23:18:31.000000000 +0000 @@ -2,19 +2,22 @@ language: go go_import_path: blitiri.com.ar/go/chasquid +dist: trusty +sudo: false go: - - 1.7 - - 1.8 - - tip - + - 1.7 # Debian stable. + - stable + - master # This is needed because the repository has a Makefile, so travis won't invoke # "go get" by default. install: - go get blitiri.com.ar/go/chasquid - go get blitiri.com.ar/go/chasquid/cmd/chasquid-util - + - go get blitiri.com.ar/go/chasquid/cmd/mda-lmtp + - go get blitiri.com.ar/go/chasquid/cmd/smtp-check + - go get blitiri.com.ar/go/chasquid/cmd/spf-check script: - make all