1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-18 14:47:03 +00:00

docs: Add missing docstrings, adjust wording to match standard style

This patch adds a missing docstrings for exported identifiers, and
adjust some of the existing ones to match the standard style.

In some cases, the identifiers were un-exported after noticing they had
no external users.

Besides improving documentation, it also reduces the linter noise
significantly.
This commit is contained in:
Alberto Bertogli
2018-03-04 15:54:34 +00:00
parent 40ae9b5f69
commit f3b01cb493
21 changed files with 154 additions and 46 deletions

View File

@@ -56,12 +56,12 @@ func main() {
} }
commands := map[string]func(){ commands := map[string]func(){
"user-add": UserAdd, "user-add": userAdd,
"user-remove": UserRemove, "user-remove": userRemove,
"authenticate": Authenticate, "authenticate": authenticate,
"check-userdb": CheckUserDB, "check-userdb": checkUserDB,
"aliases-resolve": AliasesResolve, "aliases-resolve": aliasesResolve,
"print-config": PrintConfig, "print-config": printConfig,
} }
for cmd, f := range commands { for cmd, f := range commands {
@@ -71,6 +71,7 @@ func main() {
} }
} }
// Fatalf prints the given message, then exits the program with an error code.
func Fatalf(s string, arg ...interface{}) { func Fatalf(s string, arg ...interface{}) {
fmt.Printf(s+"\n", arg...) fmt.Printf(s+"\n", arg...)
os.Exit(1) os.Exit(1)
@@ -109,7 +110,7 @@ func userDBFromArgs(create bool) (string, string, *userdb.DB) {
} }
// chasquid-util check-userdb <domain> // chasquid-util check-userdb <domain>
func CheckUserDB() { func checkUserDB() {
_, err := userdb.Load(userDBForDomain("")) _, err := userdb.Load(userDBForDomain(""))
if err != nil { if err != nil {
Fatalf("Error loading database: %v", err) Fatalf("Error loading database: %v", err)
@@ -119,7 +120,7 @@ func CheckUserDB() {
} }
// chasquid-util user-add <username> [--password=<password>] // chasquid-util user-add <username> [--password=<password>]
func UserAdd() { func userAdd() {
user, _, db := userDBFromArgs(true) user, _, db := userDBFromArgs(true)
password := getPassword() password := getPassword()
@@ -137,7 +138,7 @@ func UserAdd() {
} }
// chasquid-util authenticate <username> [--password=<password>] // chasquid-util authenticate <username> [--password=<password>]
func Authenticate() { func authenticate() {
user, _, db := userDBFromArgs(false) user, _, db := userDBFromArgs(false)
password := getPassword() password := getPassword()
@@ -177,7 +178,7 @@ func getPassword() string {
} }
// chasquid-util user-remove <username> // chasquid-util user-remove <username>
func UserRemove() { func userRemove() {
user, _, db := userDBFromArgs(false) user, _, db := userDBFromArgs(false)
present := db.RemoveUser(user) present := db.RemoveUser(user)
@@ -194,7 +195,7 @@ func UserRemove() {
} }
// chasquid-util aliases-resolve <address> // chasquid-util aliases-resolve <address>
func AliasesResolve() { func aliasesResolve() {
conf, err := config.Load(configDir + "/chasquid.conf") conf, err := config.Load(configDir + "/chasquid.conf")
if err != nil { if err != nil {
Fatalf("Error reading config") Fatalf("Error reading config")
@@ -238,7 +239,7 @@ func AliasesResolve() {
} }
// chasquid-util print-config // chasquid-util print-config
func PrintConfig() { func printConfig() {
conf, err := config.Load(configDir + "/chasquid.conf") conf, err := config.Load(configDir + "/chasquid.conf")
if err != nil { if err != nil {
Fatalf("Error reading config") Fatalf("Error reading config")

View File

@@ -72,6 +72,7 @@ type Recipient struct {
Type RType Type RType
} }
// RType represents a recipient type, see the contants below for valid values.
type RType string type RType string
// Valid recipient types. // Valid recipient types.
@@ -81,6 +82,8 @@ const (
) )
var ( var (
// ErrRecursionLimitExceeded is returned when the resolving lookup
// exceeded the recursion limit. Usually caused by aliases loops.
ErrRecursionLimitExceeded = fmt.Errorf("recursion limit exceeded") ErrRecursionLimitExceeded = fmt.Errorf("recursion limit exceeded")
// How many levels of recursions we allow during lookups. // How many levels of recursions we allow during lookups.
@@ -109,6 +112,7 @@ type Resolver struct {
mu sync.Mutex mu sync.Mutex
} }
// NewResolver returns a new, empty Resolver.
func NewResolver() *Resolver { func NewResolver() *Resolver {
return &Resolver{ return &Resolver{
files: map[string][]string{}, files: map[string][]string{},
@@ -117,6 +121,8 @@ func NewResolver() *Resolver {
} }
} }
// Resolve the given address, returning the list of corresponding recipients
// (if any).
func (v *Resolver) Resolve(addr string) ([]Recipient, error) { func (v *Resolver) Resolve(addr string) ([]Recipient, error) {
v.mu.Lock() v.mu.Lock()
defer v.mu.Unlock() defer v.mu.Unlock()
@@ -184,14 +190,17 @@ func (v *Resolver) cleanIfLocal(addr string) string {
return user + "@" + domain return user + "@" + domain
} }
// AddDomain to the resolver, registering its existence.
func (v *Resolver) AddDomain(domain string) { func (v *Resolver) AddDomain(domain string) {
v.mu.Lock() v.mu.Lock()
v.domains[domain] = true v.domains[domain] = true
v.mu.Unlock() v.mu.Unlock()
} }
// AddAliasesFile to the resolver. The file will be parsed, and an error
// returned if it does not exist or parse correctly.
func (v *Resolver) AddAliasesFile(domain, path string) error { func (v *Resolver) AddAliasesFile(domain, path string) error {
// We inconditionally add the domain and file on our list. // We unconditionally add the domain and file on our list.
// Even if the file does not exist now, it may later. This makes it be // Even if the file does not exist now, it may later. This makes it be
// consider when doing Reload. // consider when doing Reload.
// Adding it to the domains mean that we will do drop character and suffix // Adding it to the domains mean that we will do drop character and suffix
@@ -219,10 +228,13 @@ func (v *Resolver) AddAliasesFile(domain, path string) error {
return nil return nil
} }
// AddAliasForTesting adds an alias to the resolver, for testing purposes.
// Not for use in production code.
func (v *Resolver) AddAliasForTesting(addr, rcpt string, rType RType) { func (v *Resolver) AddAliasForTesting(addr, rcpt string, rType RType) {
v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, rType}) v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, rType})
} }
// Reload aliases files for all known domains.
func (v *Resolver) Reload() error { func (v *Resolver) Reload() error {
newAliases := map[string][]Recipient{} newAliases := map[string][]Recipient{}

View File

@@ -13,16 +13,16 @@ import (
"blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/normalize"
) )
// Interface for authentication backends. // Backend is the common interface for all authentication backends.
type Backend interface { type Backend interface {
Authenticate(user, password string) (bool, error) Authenticate(user, password string) (bool, error)
Exists(user string) (bool, error) Exists(user string) (bool, error)
Reload() error Reload() error
} }
// Interface for authentication backends that don't need to emit errors. // NoErrorBackend is the interface for authentication backends that don't need
// This allows backends to avoid unnecessary complexity, in exchange for a bit // to emit errors. This allows backends to avoid unnecessary complexity, in
// more here. // exchange for a bit more here.
// They can be converted to normal Backend using WrapNoErrorBackend (defined // They can be converted to normal Backend using WrapNoErrorBackend (defined
// below). // below).
type NoErrorBackend interface { type NoErrorBackend interface {
@@ -31,6 +31,8 @@ type NoErrorBackend interface {
Reload() error Reload() error
} }
// Authenticator tracks the backends for each domain, and allows callers to
// query them with a more practical API.
type Authenticator struct { type Authenticator struct {
// Registered backends, map of domain (string) -> Backend. // Registered backends, map of domain (string) -> Backend.
// Backend operations will _not_ include the domain in the username. // Backend operations will _not_ include the domain in the username.
@@ -48,6 +50,7 @@ type Authenticator struct {
AuthDuration time.Duration AuthDuration time.Duration
} }
// NewAuthenticator returns a new Authenticator with no backends.
func NewAuthenticator() *Authenticator { func NewAuthenticator() *Authenticator {
return &Authenticator{ return &Authenticator{
backends: map[string]Backend{}, backends: map[string]Backend{},
@@ -55,6 +58,7 @@ func NewAuthenticator() *Authenticator {
} }
} }
// Register a backend to use for the given domain.
func (a *Authenticator) Register(domain string, be Backend) { func (a *Authenticator) Register(domain string, be Backend) {
a.backends[domain] = be a.backends[domain] = be
} }
@@ -87,6 +91,7 @@ func (a *Authenticator) Authenticate(user, domain, password string) (bool, error
return false, nil return false, nil
} }
// Exists checks that user@domain exists.
func (a *Authenticator) Exists(user, domain string) (bool, error) { func (a *Authenticator) Exists(user, domain string) (bool, error) {
if be, ok := a.backends[domain]; ok { if be, ok := a.backends[domain]; ok {
ok, err := be.Exists(user) ok, err := be.Exists(user)

View File

@@ -81,6 +81,7 @@ func Load(path string) (*Config, error) {
return c, nil return c, nil
} }
// LogConfig logs the given configuration, in a human-friendly way.
func LogConfig(c *Config) { func LogConfig(c *Config) {
log.Infof("Configuration:") log.Infof("Configuration:")
log.Infof(" Hostname: %q", c.Hostname) log.Infof(" Hostname: %q", c.Hostname)

View File

@@ -29,6 +29,8 @@ type Procmail struct {
Timeout time.Duration // Timeout for each invocation. Timeout time.Duration // Timeout for each invocation.
} }
// Deliver an email. On failures, returns an error, and whether or not it is
// permanent.
func (p *Procmail) Deliver(from string, to string, data []byte) (error, bool) { func (p *Procmail) Deliver(from string, to string, data []byte) (error, bool) {
tr := trace.New("Courier.Procmail", to) tr := trace.New("Courier.Procmail", to)
defer tr.Finish() defer tr.Finish()

View File

@@ -41,6 +41,8 @@ type SMTP struct {
Dinfo *domaininfo.DB Dinfo *domaininfo.DB
} }
// Deliver an email. On failures, returns an error, and whether or not it is
// permanent.
func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) { func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
a := &attempt{ a := &attempt{
courier: s, courier: s,

View File

@@ -13,6 +13,7 @@ import (
// Command to generate domaininfo.pb.go. // Command to generate domaininfo.pb.go.
//go:generate protoc --go_out=. domaininfo.proto //go:generate protoc --go_out=. domaininfo.proto
// DB represents the persistent domain information database.
type DB struct { type DB struct {
// Persistent store with the list of domains we know. // Persistent store with the list of domains we know.
store *protoio.Store store *protoio.Store
@@ -23,6 +24,8 @@ type DB struct {
ev *trace.EventLog ev *trace.EventLog
} }
// New opens a domain information database on the given dir, creating it if
// necessary. The returned database will not be loaded.
func New(dir string) (*DB, error) { func New(dir string) (*DB, error) {
st, err := protoio.NewStore(dir) st, err := protoio.NewStore(dir)
if err != nil { if err != nil {

View File

@@ -21,12 +21,12 @@ import (
"unicode" "unicode"
) )
// Default timeout to use. We expect Dovecot to be quite fast, but don't want // DefaultTimeout to use. We expect Dovecot to be quite fast, but don't want
// to hang forever if something gets stuck. // to hang forever if something gets stuck.
const DefaultTimeout = 5 * time.Second const DefaultTimeout = 5 * time.Second
var ( var (
ErrUsernameNotSafe = errors.New("username not safe (contains spaces)") errUsernameNotSafe = errors.New("username not safe (contains spaces)")
) )
var defaultUserdbPaths = []string{ var defaultUserdbPaths = []string{
@@ -60,6 +60,7 @@ func NewAuth(userdb, client string) *Auth {
} }
} }
// String representation of this Auth, for human consumption.
func (a *Auth) String() string { func (a *Auth) String() string {
return fmt.Sprintf("DovecotAuth(%q, %q)", a.userdbAddr, a.clientAddr) return fmt.Sprintf("DovecotAuth(%q, %q)", a.userdbAddr, a.clientAddr)
} }
@@ -79,10 +80,10 @@ func (a *Auth) Check() error {
return nil return nil
} }
// Does user exist? // Exists returns true if the user exists, false otherwise.
func (a *Auth) Exists(user string) (bool, error) { func (a *Auth) Exists(user string) (bool, error) {
if !isUsernameSafe(user) { if !isUsernameSafe(user) {
return false, ErrUsernameNotSafe return false, errUsernameNotSafe
} }
conn, err := a.dial("unix", a.userdbAddr) conn, err := a.dial("unix", a.userdbAddr)
@@ -126,10 +127,11 @@ func (a *Auth) Exists(user string) (bool, error) {
return false, fmt.Errorf("invalid response: %q", resp) return false, fmt.Errorf("invalid response: %q", resp)
} }
// Is the password valud for the user? // Authenticate returns true if the password is valid for the user, false
// otherwise.
func (a *Auth) Authenticate(user, passwd string) (bool, error) { func (a *Auth) Authenticate(user, passwd string) (bool, error) {
if !isUsernameSafe(user) { if !isUsernameSafe(user) {
return false, ErrUsernameNotSafe return false, errUsernameNotSafe
} }
conn, err := a.dial("unix", a.clientAddr) conn, err := a.dial("unix", a.clientAddr)
@@ -182,6 +184,8 @@ func (a *Auth) Authenticate(user, passwd string) (bool, error) {
return false, fmt.Errorf("invalid response: %q", resp) return false, fmt.Errorf("invalid response: %q", resp)
} }
// Reload the authenticator. It's a no-op for dovecot, but it is needed to
// conform with the auth.Backend interface.
func (a *Auth) Reload() error { func (a *Auth) Reload() error {
return nil return nil
} }

View File

@@ -19,12 +19,12 @@ func TestUsernameNotSafe(t *testing.T) {
"a b", " ab", "ab ", "a\tb", "a\t", " ", "\t", "\t "} "a b", " ab", "ab ", "a\tb", "a\t", " ", "\t", "\t "}
for _, c := range cases { for _, c := range cases {
ok, err := a.Authenticate(c, "passwd") ok, err := a.Authenticate(c, "passwd")
if ok || err != ErrUsernameNotSafe { if ok || err != errUsernameNotSafe {
t.Errorf("Authenticate(%q, _): got %v, %v", c, ok, err) t.Errorf("Authenticate(%q, _): got %v, %v", c, ok, err)
} }
ok, err = a.Exists(c) ok, err = a.Exists(c)
if ok || err != ErrUsernameNotSafe { if ok || err != errUsernameNotSafe {
t.Errorf("Exists(%q): got %v, %v", c, ok, err) t.Errorf("Exists(%q): got %v, %v", c, ok, err)
} }
} }

View File

@@ -19,16 +19,19 @@ func Split(addr string) (string, string) {
return ps[0], ps[1] return ps[0], ps[1]
} }
// UserOf user@domain returns user.
func UserOf(addr string) string { func UserOf(addr string) string {
user, _ := Split(addr) user, _ := Split(addr)
return user return user
} }
// DomainOf user@domain returns domain.
func DomainOf(addr string) string { func DomainOf(addr string) string {
_, domain := Split(addr) _, domain := Split(addr)
return domain return domain
} }
// DomainIn checks that the domain of the address is on the given set.
func DomainIn(addr string, locals *set.String) bool { func DomainIn(addr string, locals *set.String) bool {
domain := DomainOf(addr) domain := DomainOf(addr)
if domain == "" { if domain == "" {
@@ -38,6 +41,7 @@ func DomainIn(addr string, locals *set.String) bool {
return locals.Has(domain) return locals.Has(domain)
} }
// AddHeader adds (prepends) a MIME header to the message.
func AddHeader(data []byte, k, v string) []byte { func AddHeader(data []byte, k, v string) []byte {
if len(v) > 0 { if len(v) > 0 {
// If the value contains newlines, indent them properly. // If the value contains newlines, indent them properly.

View File

@@ -24,20 +24,26 @@ type timedWriter struct {
w io.Writer w io.Writer
} }
// Write the given buffer, prepending timing information.
func (t timedWriter) Write(b []byte) (int, error) { func (t timedWriter) Write(b []byte) (int, error) {
fmt.Fprintf(t.w, "%s ", time.Now().Format("2006-01-02 15:04:05.000000")) fmt.Fprintf(t.w, "%s ", time.Now().Format("2006-01-02 15:04:05.000000"))
return t.w.Write(b) return t.w.Write(b)
} }
// Logger contains a backend used to log data to, such as a file or syslog.
// It implements various user-friendly methods for logging mail information to
// it.
type Logger struct { type Logger struct {
w io.Writer w io.Writer
once sync.Once once sync.Once
} }
// New creates a new Logger which will write messages to the given writer.
func New(w io.Writer) *Logger { func New(w io.Writer) *Logger {
return &Logger{w: timedWriter{w}} return &Logger{w: timedWriter{w}}
} }
// NewSyslog creates a new Logger which will write messages to syslog.
func NewSyslog() (*Logger, error) { func NewSyslog() (*Logger, error) {
w, err := syslog.New(syslog.LOG_INFO|syslog.LOG_MAIL, "chasquid") w, err := syslog.New(syslog.LOG_INFO|syslog.LOG_MAIL, "chasquid")
if err != nil { if err != nil {
@@ -58,10 +64,12 @@ func (l *Logger) printf(format string, args ...interface{}) {
} }
} }
// Listening logs that the daemon is listening on the given address.
func (l *Logger) Listening(a string) { func (l *Logger) Listening(a string) {
l.printf("daemon listening on %s\n", a) l.printf("daemon listening on %s\n", a)
} }
// Auth logs an authentication request.
func (l *Logger) Auth(netAddr net.Addr, user string, successful bool) { func (l *Logger) Auth(netAddr net.Addr, user string, successful bool) {
res := "succeeded" res := "succeeded"
if !successful { if !successful {
@@ -72,6 +80,7 @@ func (l *Logger) Auth(netAddr net.Addr, user string, successful bool) {
authLog.Debugf(msg) authLog.Debugf(msg)
} }
// Rejected logs that we've rejected an email.
func (l *Logger) Rejected(netAddr net.Addr, from string, to []string, err string) { func (l *Logger) Rejected(netAddr net.Addr, from string, to []string, err string) {
if from != "" { if from != "" {
from = fmt.Sprintf(" from=%s", from) from = fmt.Sprintf(" from=%s", from)
@@ -83,10 +92,12 @@ func (l *Logger) Rejected(netAddr net.Addr, from string, to []string, err string
l.printf("%s rejected%s%s - %v\n", netAddr, from, toStr, err) l.printf("%s rejected%s%s - %v\n", netAddr, from, toStr, err)
} }
// Queued logs that we have queued an email.
func (l *Logger) Queued(netAddr net.Addr, from string, to []string, id string) { func (l *Logger) Queued(netAddr net.Addr, from string, to []string, id string) {
l.printf("%s from=%s queued ip=%s to=%v\n", id, from, netAddr, to) l.printf("%s from=%s queued ip=%s to=%v\n", id, from, netAddr, to)
} }
// SendAttempt logs that we have attempted to send an email.
func (l *Logger) SendAttempt(id, from, to string, err error, permanent bool) { func (l *Logger) SendAttempt(id, from, to string, err error, permanent bool) {
if err == nil { if err == nil {
l.printf("%s from=%s to=%s sent\n", id, from, to) l.printf("%s from=%s to=%s sent\n", id, from, to)
@@ -99,6 +110,7 @@ func (l *Logger) SendAttempt(id, from, to string, err error, permanent bool) {
} }
} }
// QueueLoop logs that we have completed a queue loop.
func (l *Logger) QueueLoop(id, from string, nextDelay time.Duration) { func (l *Logger) QueueLoop(id, from string, nextDelay time.Duration) {
if nextDelay > 0 { if nextDelay > 0 {
l.printf("%s from=%s completed loop, next in %v\n", id, from, nextDelay) l.printf("%s from=%s completed loop, next in %v\n", id, from, nextDelay)
@@ -107,29 +119,35 @@ func (l *Logger) QueueLoop(id, from string, nextDelay time.Duration) {
} }
} }
// The default logger used in the following top-level functions. // Default logger, used in the following top-level functions.
var Default *Logger = New(ioutil.Discard) var Default = New(ioutil.Discard)
// Listening logs that the daemon is listening on the given address.
func Listening(a string) { func Listening(a string) {
Default.Listening(a) Default.Listening(a)
} }
// Auth logs an authentication request.
func Auth(netAddr net.Addr, user string, successful bool) { func Auth(netAddr net.Addr, user string, successful bool) {
Default.Auth(netAddr, user, successful) Default.Auth(netAddr, user, successful)
} }
// Rejected logs that we've rejected an email.
func Rejected(netAddr net.Addr, from string, to []string, err string) { func Rejected(netAddr net.Addr, from string, to []string, err string) {
Default.Rejected(netAddr, from, to, err) Default.Rejected(netAddr, from, to, err)
} }
// Queued logs that we have queued an email.
func Queued(netAddr net.Addr, from string, to []string, id string) { func Queued(netAddr net.Addr, from string, to []string, id string) {
Default.Queued(netAddr, from, to, id) Default.Queued(netAddr, from, to, id)
} }
// SendAttempt logs that we have attempted to send an email.
func SendAttempt(id, from, to string, err error, permanent bool) { func SendAttempt(id, from, to string, err error, permanent bool) {
Default.SendAttempt(id, from, to, err, permanent) Default.SendAttempt(id, from, to, err, permanent)
} }
// QueueLoop logs that we have completed a queue loop.
func QueueLoop(id, from string, nextDelay time.Duration) { func QueueLoop(id, from string, nextDelay time.Duration) {
Default.QueueLoop(id, from, nextDelay) Default.QueueLoop(id, from, nextDelay)
} }

View File

@@ -41,7 +41,7 @@ func Domain(domain string) (string, error) {
return d, nil return d, nil
} }
// Name normalizes an email address, applying User and Domain to its // Addr normalizes an email address, applying User and Domain to its
// respective components. // respective components.
// On error, it will also return the original address to simplify callers. // On error, it will also return the original address to simplify callers.
func Addr(addr string) (string, error) { func Addr(addr string) (string, error) {
@@ -60,8 +60,8 @@ func Addr(addr string) (string, error) {
return user + "@" + domain, nil return user + "@" + domain, nil
} }
// Take an address with an ASCII domain, and convert it to Unicode as per // DomainToUnicode takes an address with an ASCII domain, and convert it to
// IDNA, including basic normalization. // Unicode as per IDNA, including basic normalization.
// The user part is unchanged. // The user part is unchanged.
func DomainToUnicode(addr string) (string, error) { func DomainToUnicode(addr string) (string, error) {
if addr == "<>" { if addr == "<>" {

View File

@@ -72,10 +72,12 @@ func (s *Store) idToFname(id string) string {
return s.dir + "/" + storeIDPrefix + url.QueryEscape(id) return s.dir + "/" + storeIDPrefix + url.QueryEscape(id)
} }
// Put a message into the store.
func (s *Store) Put(id string, m proto.Message) error { func (s *Store) Put(id string, m proto.Message) error {
return WriteTextMessage(s.idToFname(id), m, 0660) return WriteTextMessage(s.idToFname(id), m, 0660)
} }
// Get a message from the store.
func (s *Store) Get(id string, m proto.Message) (bool, error) { func (s *Store) Get(id string, m proto.Message) (bool, error) {
err := ReadTextMessage(s.idToFname(id), m) err := ReadTextMessage(s.idToFname(id), m)
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -84,6 +86,7 @@ func (s *Store) Get(id string, m proto.Message) (bool, error) {
return err == nil, err return err == nil, err
} }
// ListIDs in the store.
func (s *Store) ListIDs() ([]string, error) { func (s *Store) ListIDs() ([]string, error) {
ids := []string{} ids := []string{}

View File

@@ -146,6 +146,7 @@ func (q *Queue) Load() error {
return nil return nil
} }
// Len returns the number of elements in the queue.
func (q *Queue) Len() int { func (q *Queue) Len() int {
q.mu.RLock() q.mu.RLock()
defer q.mu.RUnlock() defer q.mu.RUnlock()
@@ -253,7 +254,7 @@ func (q *Queue) DumpString() string {
return s return s
} }
// An item in the queue. // An Item in the queue.
type Item struct { type Item struct {
// Base the item on the protobuf message. // Base the item on the protobuf message.
// We will use this for serialization, so any fields below are NOT // We will use this for serialization, so any fields below are NOT
@@ -267,6 +268,7 @@ type Item struct {
CreatedAt time.Time CreatedAt time.Time
} }
// ItemFromFile loads an item from the given file.
func ItemFromFile(fname string) (*Item, error) { func ItemFromFile(fname string) (*Item, error) {
item := &Item{} item := &Item{}
err := protoio.ReadTextMessage(fname, &item.Message) err := protoio.ReadTextMessage(fname, &item.Message)
@@ -278,6 +280,7 @@ func ItemFromFile(fname string) (*Item, error) {
return item, err return item, err
} }
// WriteTo saves an item to the given directory.
func (item *Item) WriteTo(dir string) error { func (item *Item) WriteTo(dir string) error {
item.Lock() item.Lock()
defer item.Unlock() defer item.Unlock()
@@ -294,6 +297,7 @@ func (item *Item) WriteTo(dir string) error {
return protoio.WriteTextMessage(path, &item.Message, 0600) return protoio.WriteTextMessage(path, &item.Message, 0600)
} }
// SendLoop repeatedly attempts to send the item.
func (item *Item) SendLoop(q *Queue) { func (item *Item) SendLoop(q *Queue) {
tr := trace.New("Queue.SendLoop", item.ID) tr := trace.New("Queue.SendLoop", item.ID)
defer tr.Finish() defer tr.Finish()

View File

@@ -9,7 +9,7 @@ import (
"syscall" "syscall"
) )
// Type FileOp represents an operation on a file (passed by its name). // FileOp represents an operation on a file (passed by its name).
type FileOp func(fname string) error type FileOp func(fname string) error
// WriteFile writes data to a file named by filename, atomically. // WriteFile writes data to a file named by filename, atomically.

View File

@@ -1,16 +1,19 @@
// Package set implement sets for various types. Well, only string for now :) // Package set implement sets for various types. Well, only string for now :)
package set package set
// String set.
type String struct { type String struct {
m map[string]struct{} m map[string]struct{}
} }
// NewString returns a new string set, with the given values in it.
func NewString(values ...string) *String { func NewString(values ...string) *String {
s := &String{} s := &String{}
s.Add(values...) s.Add(values...)
return s return s
} }
// Add values to the string set.
func (s *String) Add(values ...string) { func (s *String) Add(values ...string) {
if s.m == nil { if s.m == nil {
s.m = map[string]struct{}{} s.m = map[string]struct{}{}
@@ -21,6 +24,7 @@ func (s *String) Add(values ...string) {
} }
} }
// Has checks if the set has the given value.
func (s *String) Has(value string) bool { func (s *String) Has(value string) bool {
// We explicitly allow s to be nil *in this function* to simplify callers' // We explicitly allow s to be nil *in this function* to simplify callers'
// code. Note that Add will not tolerate it, and will panic. // code. Note that Add will not tolerate it, and will panic.

View File

@@ -22,6 +22,7 @@ type Client struct {
*smtp.Client *smtp.Client
} }
// NewClient uses the given connection to create a new Client.
func NewClient(conn net.Conn, host string) (*Client, error) { func NewClient(conn net.Conn, host string) (*Client, error) {
c, err := smtp.NewClient(conn, host) c, err := smtp.NewClient(conn, host)
if err != nil { if err != nil {
@@ -129,7 +130,7 @@ func isASCII(s string) bool {
return true return true
} }
// ErrIsPermanent returns true if the error is permanent, and false otherwise. // IsPermanent returns true if the error is permanent, and false otherwise.
// If it can't tell, it returns false. // If it can't tell, it returns false.
func IsPermanent(err error) bool { func IsPermanent(err error) bool {
terr, ok := err.(*textproto.Error) terr, ok := err.(*textproto.Error)

View File

@@ -52,7 +52,7 @@ var (
disableSPFForTesting = false disableSPFForTesting = false
) )
// Mode for a socket (listening or connection). // SocketMode represents the mode for a socket (listening or connection).
// We keep them distinct, as policies can differ between them. // We keep them distinct, as policies can differ between them.
type SocketMode struct { type SocketMode struct {
// Is this mode submission? // Is this mode submission?
@@ -81,7 +81,7 @@ var (
ModeSubmissionTLS = SocketMode{IsSubmission: true, TLS: true} ModeSubmissionTLS = SocketMode{IsSubmission: true, TLS: true}
) )
// Incoming SMTP connection. // Conn represents an incoming SMTP connection.
type Conn struct { type Conn struct {
// Main hostname, used for display only. // Main hostname, used for display only.
hostname string hostname string
@@ -146,10 +146,13 @@ type Conn struct {
commandTimeout time.Duration commandTimeout time.Duration
} }
// Close the connection.
func (c *Conn) Close() { func (c *Conn) Close() {
c.conn.Close() c.conn.Close()
} }
// Handle implements the main protocol loop (reading commands, sending
// replies).
func (c *Conn) Handle() { func (c *Conn) Handle() {
defer c.Close() defer c.Close()
@@ -265,6 +268,7 @@ loop:
} }
} }
// HELO SMTP command handler.
func (c *Conn) HELO(params string) (code int, msg string) { func (c *Conn) HELO(params string) (code int, msg string) {
if len(strings.TrimSpace(params)) == 0 { if len(strings.TrimSpace(params)) == 0 {
return 501, "Invisible customers are not welcome!" return 501, "Invisible customers are not welcome!"
@@ -282,6 +286,7 @@ func (c *Conn) HELO(params string) (code int, msg string) {
return 250, msg return 250, msg
} }
// EHLO SMTP command handler.
func (c *Conn) EHLO(params string) (code int, msg string) { func (c *Conn) EHLO(params string) (code int, msg string) {
if len(strings.TrimSpace(params)) == 0 { if len(strings.TrimSpace(params)) == 0 {
return 501, "Invisible customers are not welcome!" return 501, "Invisible customers are not welcome!"
@@ -303,10 +308,12 @@ func (c *Conn) EHLO(params string) (code int, msg string) {
return 250, buf.String() return 250, buf.String()
} }
// HELP SMTP command handler.
func (c *Conn) HELP(params string) (code int, msg string) { func (c *Conn) HELP(params string) (code int, msg string) {
return 214, "hoy por ti, mañana por mi" return 214, "hoy por ti, mañana por mi"
} }
// RSET SMTP command handler.
func (c *Conn) RSET(params string) (code int, msg string) { func (c *Conn) RSET(params string) (code int, msg string) {
c.resetEnvelope() c.resetEnvelope()
@@ -319,6 +326,7 @@ func (c *Conn) RSET(params string) (code int, msg string) {
return 250, msgs[rand.Int()%len(msgs)] return 250, msgs[rand.Int()%len(msgs)]
} }
// VRFY SMTP command handler.
func (c *Conn) VRFY(params string) (code int, msg string) { func (c *Conn) VRFY(params string) (code int, msg string) {
// 252 can be used for cases like ours, when we don't really want to // 252 can be used for cases like ours, when we don't really want to
// confirm or deny anything. // confirm or deny anything.
@@ -326,6 +334,7 @@ func (c *Conn) VRFY(params string) (code int, msg string) {
return 252, "You have a strange feeling for a moment, then it passes." return 252, "You have a strange feeling for a moment, then it passes."
} }
// EXPN SMTP command handler.
func (c *Conn) EXPN(params string) (code int, msg string) { func (c *Conn) EXPN(params string) (code int, msg string) {
// 252 can be used for cases like ours, when we don't really want to // 252 can be used for cases like ours, when we don't really want to
// confirm or deny anything. // confirm or deny anything.
@@ -333,10 +342,12 @@ func (c *Conn) EXPN(params string) (code int, msg string) {
return 252, "You feel disoriented for a moment." return 252, "You feel disoriented for a moment."
} }
// NOOP SMTP command handler.
func (c *Conn) NOOP(params string) (code int, msg string) { func (c *Conn) NOOP(params string) (code int, msg string) {
return 250, "You hear a faint typing noise." return 250, "You hear a faint typing noise."
} }
// MAIL SMTP command handler.
func (c *Conn) MAIL(params string) (code int, msg string) { func (c *Conn) MAIL(params string) (code int, msg string) {
// params should be: "FROM:<name@host>", and possibly followed by // params should be: "FROM:<name@host>", and possibly followed by
// options such as "BODY=8BITMIME" (which we ignore). // options such as "BODY=8BITMIME" (which we ignore).
@@ -466,6 +477,7 @@ func (c *Conn) secLevelCheck(addr string) bool {
return ok return ok
} }
// RCPT SMTP command handler.
func (c *Conn) RCPT(params string) (code int, msg string) { func (c *Conn) RCPT(params string) (code int, msg string) {
// params should be: "TO:<name@host>", and possibly followed by options // params should be: "TO:<name@host>", and possibly followed by options
// such as "NOTIFY=SUCCESS,DELAY" (which we ignore). // such as "NOTIFY=SUCCESS,DELAY" (which we ignore).
@@ -531,6 +543,7 @@ func (c *Conn) RCPT(params string) (code int, msg string) {
return 250, "You have an eerie feeling..." return 250, "You have an eerie feeling..."
} }
// DATA SMTP command handler.
func (c *Conn) DATA(params string) (code int, msg string) { func (c *Conn) DATA(params string) (code int, msg string) {
if c.ehloAddress == "" { if c.ehloAddress == "" {
return 503, "Invisible customers are not welcome!" return 503, "Invisible customers are not welcome!"
@@ -796,6 +809,7 @@ func boolToStr(b bool) string {
return "0" return "0"
} }
// STARTTLS SMTP command handler.
func (c *Conn) STARTTLS(params string) (code int, msg string) { func (c *Conn) STARTTLS(params string) (code int, msg string) {
if c.onTLS { if c.onTLS {
return 503, "You are already wearing that!" return 503, "You are already wearing that!"
@@ -840,6 +854,7 @@ func (c *Conn) STARTTLS(params string) (code int, msg string) {
return 0, "" return 0, ""
} }
// AUTH SMTP command handler.
func (c *Conn) AUTH(params string) (code int, msg string) { func (c *Conn) AUTH(params string) (code int, msg string) {
if !c.onTLS { if !c.onTLS {
return 503, "You feel vulnerable" return 503, "You feel vulnerable"

View File

@@ -28,6 +28,7 @@ var (
"how often to reload, ONLY FOR TESTING") "how often to reload, ONLY FOR TESTING")
) )
// Server represents an SMTP server instance.
type Server struct { type Server struct {
// Main hostname, used for display only. // Main hostname, used for display only.
Hostname string Hostname string
@@ -70,6 +71,7 @@ type Server struct {
PostDataHook string PostDataHook string
} }
// NewServer returns a new empty Server.
func NewServer() *Server { func NewServer() *Server {
return &Server{ return &Server{
addrs: map[SocketMode][]string{}, addrs: map[SocketMode][]string{},
@@ -83,6 +85,7 @@ func NewServer() *Server {
} }
} }
// AddCerts (TLS) to the server.
func (s *Server) AddCerts(certPath, keyPath string) error { func (s *Server) AddCerts(certPath, keyPath string) error {
cert, err := tls.LoadX509KeyPair(certPath, keyPath) cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil { if err != nil {
@@ -92,36 +95,44 @@ func (s *Server) AddCerts(certPath, keyPath string) error {
return nil return nil
} }
// AddAddr adds an address for the server to listen on.
func (s *Server) AddAddr(a string, m SocketMode) { func (s *Server) AddAddr(a string, m SocketMode) {
s.addrs[m] = append(s.addrs[m], a) s.addrs[m] = append(s.addrs[m], a)
} }
// AddListeners adds listeners for the server to listen on.
func (s *Server) AddListeners(ls []net.Listener, m SocketMode) { func (s *Server) AddListeners(ls []net.Listener, m SocketMode) {
s.listeners[m] = append(s.listeners[m], ls...) s.listeners[m] = append(s.listeners[m], ls...)
} }
// AddDomain adds a local domain to the server.
func (s *Server) AddDomain(d string) { func (s *Server) AddDomain(d string) {
s.localDomains.Add(d) s.localDomains.Add(d)
s.aliasesR.AddDomain(d) s.aliasesR.AddDomain(d)
} }
// AddUserDB adds a userdb.DB instance as backend for the domain.
func (s *Server) AddUserDB(domain string, db *userdb.DB) { func (s *Server) AddUserDB(domain string, db *userdb.DB) {
s.authr.Register(domain, auth.WrapNoErrorBackend(db)) s.authr.Register(domain, auth.WrapNoErrorBackend(db))
} }
// AddAliasesFile adds an aliases file for the given domain.
func (s *Server) AddAliasesFile(domain, f string) error { func (s *Server) AddAliasesFile(domain, f string) error {
return s.aliasesR.AddAliasesFile(domain, f) return s.aliasesR.AddAliasesFile(domain, f)
} }
// SetAuthFallback sets the authentication backend to use as fallback.
func (s *Server) SetAuthFallback(be auth.Backend) { func (s *Server) SetAuthFallback(be auth.Backend) {
s.authr.Fallback = be s.authr.Fallback = be
} }
// SetAliasesConfig sets the aliases configuration options.
func (s *Server) SetAliasesConfig(suffixSep, dropChars string) { func (s *Server) SetAliasesConfig(suffixSep, dropChars string) {
s.aliasesR.SuffixSep = suffixSep s.aliasesR.SuffixSep = suffixSep
s.aliasesR.DropChars = dropChars s.aliasesR.DropChars = dropChars
} }
// InitDomainInfo initializes the domain info database.
func (s *Server) InitDomainInfo(dir string) *domaininfo.DB { func (s *Server) InitDomainInfo(dir string) *domaininfo.DB {
var err error var err error
s.dinfo, err = domaininfo.New(dir) s.dinfo, err = domaininfo.New(dir)
@@ -137,6 +148,7 @@ func (s *Server) InitDomainInfo(dir string) *domaininfo.DB {
return s.dinfo return s.dinfo
} }
// InitQueue initializes the queue.
func (s *Server) InitQueue(path string, localC, remoteC courier.Courier) { func (s *Server) InitQueue(path string, localC, remoteC courier.Courier) {
q := queue.New(path, s.localDomains, s.aliasesR, localC, remoteC) q := queue.New(path, s.localDomains, s.aliasesR, localC, remoteC)
err := q.Load() err := q.Load()
@@ -151,7 +163,7 @@ func (s *Server) InitQueue(path string, localC, remoteC courier.Courier) {
}) })
} }
// PeriodicallyReload some of the server's information, such as aliases and // periodicallyReload some of the server's information, such as aliases and
// the user databases. // the user databases.
func (s *Server) periodicallyReload() { func (s *Server) periodicallyReload() {
for range time.Tick(*reloadEvery) { for range time.Tick(*reloadEvery) {
@@ -167,6 +179,8 @@ func (s *Server) periodicallyReload() {
} }
} }
// ListenAndServe on the addresses and listeners that were previously added.
// This function will not return.
func (s *Server) ListenAndServe() { func (s *Server) ListenAndServe() {
if len(s.tlsConfig.Certificates) == 0 { if len(s.tlsConfig.Certificates) == 0 {
// chasquid assumes there's at least one valid certificate (for things // chasquid assumes there's at least one valid certificate (for things

View File

@@ -10,12 +10,14 @@ import (
nettrace "golang.org/x/net/trace" nettrace "golang.org/x/net/trace"
) )
// A Trace represents an active request.
type Trace struct { type Trace struct {
family string family string
title string title string
t nettrace.Trace t nettrace.Trace
} }
// New trace.
func New(family, title string) *Trace { func New(family, title string) *Trace {
t := &Trace{family, title, nettrace.New(family, title)} t := &Trace{family, title, nettrace.New(family, title)}
@@ -26,6 +28,7 @@ func New(family, title string) *Trace {
return t return t
} }
// Printf adds this message to the trace's log.
func (t *Trace) Printf(format string, a ...interface{}) { func (t *Trace) Printf(format string, a ...interface{}) {
t.t.LazyPrintf(format, a...) t.t.LazyPrintf(format, a...)
@@ -33,6 +36,7 @@ func (t *Trace) Printf(format string, a ...interface{}) {
quote(fmt.Sprintf(format, a...))) quote(fmt.Sprintf(format, a...)))
} }
// Debugf adds this message to the trace's log, with a debugging level.
func (t *Trace) Debugf(format string, a ...interface{}) { func (t *Trace) Debugf(format string, a ...interface{}) {
t.t.LazyPrintf(format, a...) t.t.LazyPrintf(format, a...)
@@ -40,6 +44,7 @@ func (t *Trace) Debugf(format string, a ...interface{}) {
t.family, t.title, quote(fmt.Sprintf(format, a...))) t.family, t.title, quote(fmt.Sprintf(format, a...)))
} }
// Errorf adds this message to the trace's log, with an error level.
func (t *Trace) Errorf(format string, a ...interface{}) error { func (t *Trace) Errorf(format string, a ...interface{}) error {
// Note we can't just call t.Error here, as it breaks caller logging. // Note we can't just call t.Error here, as it breaks caller logging.
err := fmt.Errorf(format, a...) err := fmt.Errorf(format, a...)
@@ -51,6 +56,8 @@ func (t *Trace) Errorf(format string, a ...interface{}) error {
return err return err
} }
// Error marks the trace as having seen an error, and also logs it to the
// trace's log.
func (t *Trace) Error(err error) error { func (t *Trace) Error(err error) error {
t.t.SetError() t.t.SetError()
t.t.LazyPrintf("error: %v", err) t.t.LazyPrintf("error: %v", err)
@@ -61,20 +68,24 @@ func (t *Trace) Error(err error) error {
return err return err
} }
// Finish the trace. It should not be changed after this is called.
func (t *Trace) Finish() { func (t *Trace) Finish() {
t.t.Finish() t.t.Finish()
} }
// EventLog is used for tracing long-lived objects.
type EventLog struct { type EventLog struct {
family string family string
title string title string
e nettrace.EventLog e nettrace.EventLog
} }
// NewEventLog returns a new EventLog.
func NewEventLog(family, title string) *EventLog { func NewEventLog(family, title string) *EventLog {
return &EventLog{family, title, nettrace.NewEventLog(family, title)} return &EventLog{family, title, nettrace.NewEventLog(family, title)}
} }
// Printf adds the message to the EventLog.
func (e *EventLog) Printf(format string, a ...interface{}) { func (e *EventLog) Printf(format string, a ...interface{}) {
e.e.Printf(format, a...) e.e.Printf(format, a...)
@@ -82,6 +93,7 @@ func (e *EventLog) Printf(format string, a ...interface{}) {
quote(fmt.Sprintf(format, a...))) quote(fmt.Sprintf(format, a...)))
} }
// Debugf adds the message to the EventLog, with a debugging level.
func (e *EventLog) Debugf(format string, a ...interface{}) { func (e *EventLog) Debugf(format string, a ...interface{}) {
e.e.Printf(format, a...) e.e.Printf(format, a...)
@@ -89,6 +101,7 @@ func (e *EventLog) Debugf(format string, a ...interface{}) {
quote(fmt.Sprintf(format, a...))) quote(fmt.Sprintf(format, a...)))
} }
// Errorf adds the message to the EventLog, with an error level.
func (e *EventLog) Errorf(format string, a ...interface{}) error { func (e *EventLog) Errorf(format string, a ...interface{}) error {
err := fmt.Errorf(format, a...) err := fmt.Errorf(format, a...)
e.e.Errorf("error: %v", err) e.e.Errorf("error: %v", err)

View File

@@ -45,6 +45,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/protoio" "blitiri.com.ar/go/chasquid/internal/protoio"
) )
// DB represents a single user database.
type DB struct { type DB struct {
fname string fname string
db *ProtoDB db *ProtoDB
@@ -53,10 +54,7 @@ type DB struct {
mu sync.RWMutex mu sync.RWMutex
} }
var ( // New returns a new user database, on the given file name.
ErrInvalidUsername = errors.New("invalid username")
)
func New(fname string) *DB { func New(fname string) *DB {
return &DB{ return &DB{
fname: fname, fname: fname,
@@ -106,7 +104,8 @@ func (db *DB) Write() error {
return protoio.WriteTextMessage(db.fname, db.db, 0660) return protoio.WriteTextMessage(db.fname, db.db, 0660)
} }
// Is this password valid for the user? // Authenticate returns true if the password is valid for the user, false
// otherwise.
func (db *DB) Authenticate(name, plainPassword string) bool { func (db *DB) Authenticate(name, plainPassword string) bool {
db.mu.RLock() db.mu.RLock()
passwd, ok := db.db.Users[name] passwd, ok := db.db.Users[name]
@@ -119,6 +118,7 @@ func (db *DB) Authenticate(name, plainPassword string) bool {
return passwd.PasswordMatches(plainPassword) return passwd.PasswordMatches(plainPassword)
} }
// PasswordMatches returns true if the given password is a match.
func (p *Password) PasswordMatches(plain string) bool { func (p *Password) PasswordMatches(plain string) bool {
switch s := p.Scheme.(type) { switch s := p.Scheme.(type) {
case nil: case nil:
@@ -132,11 +132,11 @@ func (p *Password) PasswordMatches(plain string) bool {
} }
} }
// Add a user to the database. If the user is already present, override it. // AddUser to the database. If the user is already present, override it.
// Note we enforce that the name has been normalized previously. // Note we enforce that the name has been normalized previously.
func (db *DB) AddUser(name, plainPassword string) error { func (db *DB) AddUser(name, plainPassword string) error {
if norm, err := normalize.User(name); err != nil || name != norm { if norm, err := normalize.User(name); err != nil || name != norm {
return ErrInvalidUsername return errors.New("invalid username")
} }
s := &Scrypt{ s := &Scrypt{
@@ -178,7 +178,7 @@ func (db *DB) RemoveUser(name string) bool {
return present return present
} }
// Exists returns true if the user is present, False otherwise. // Exists returns true if the user is present, false otherwise.
func (db *DB) Exists(name string) bool { func (db *DB) Exists(name string) bool {
db.mu.Lock() db.mu.Lock()
_, present := db.db.Users[name] _, present := db.db.Users[name]
@@ -190,7 +190,8 @@ func (db *DB) Exists(name string) bool {
// Encryption schemes // Encryption schemes
// //
// Plain text scheme. Useful mostly for testing and debugging. // PasswordMatches implementation for the plain text scheme.
// Useful mostly for testing and debugging.
// TODO: Do we really need this? Removing it would make accidents less likely // TODO: Do we really need this? Removing it would make accidents less likely
// to happen. Consider doing so when we add another scheme, so we a least have // to happen. Consider doing so when we add another scheme, so we a least have
// two and multi-scheme support does not bit-rot. // two and multi-scheme support does not bit-rot.
@@ -198,7 +199,8 @@ func (p *Plain) PasswordMatches(plain string) bool {
return plain == string(p.Password) return plain == string(p.Password)
} }
// scrypt scheme, which we use by default. // PasswordMatches implementation for the scrypt scheme, which we use by
// default.
func (s *Scrypt) PasswordMatches(plain string) bool { func (s *Scrypt) PasswordMatches(plain string) bool {
dk, err := scrypt.Key([]byte(plain), s.Salt, dk, err := scrypt.Key([]byte(plain), s.Salt,
1<<s.LogN, int(s.R), int(s.P), int(s.KeyLen)) 1<<s.LogN, int(s.R), int(s.P), int(s.KeyLen))