1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-01-07 17:47:14 +00:00

aliases: Implement aliases hooks

This patch implements two new hooks: alias-resolve and alias-exists.

They are called during the aliases resolution process, to allow for more
complex integration with other systems, such as storing the aliases in a
database.

See the included documentation for more details.
This commit is contained in:
Alberto Bertogli
2019-10-22 22:05:09 +01:00
parent dea6f73164
commit f399fe3e84
8 changed files with 253 additions and 36 deletions

View File

@@ -55,14 +55,24 @@ package aliases
import (
"bufio"
"context"
"expvar"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"time"
"blitiri.com.ar/go/chasquid/internal/envelope"
"blitiri.com.ar/go/chasquid/internal/normalize"
"blitiri.com.ar/go/chasquid/internal/trace"
)
// Exported variables.
var (
hookResults = expvar.NewMap("chasquid/aliases/hookResults")
)
// Recipient represents a single recipient, after resolving aliases.
@@ -101,6 +111,10 @@ type Resolver struct {
// Characters to drop from the user part.
DropChars string
// Path to resolve and exist hooks.
ExistsHook string
ResolveHook string
// Map of domain -> alias files for that domain.
// We keep track of them for reloading purposes.
files map[string][]string
@@ -125,9 +139,6 @@ func NewResolver() *Resolver {
// Resolve the given address, returning the list of corresponding recipients
// (if any).
func (v *Resolver) Resolve(addr string) ([]Recipient, error) {
v.mu.Lock()
defer v.mu.Unlock()
return v.resolve(0, addr)
}
@@ -137,11 +148,15 @@ func (v *Resolver) Resolve(addr string) ([]Recipient, error) {
// doesn't exist.
func (v *Resolver) Exists(addr string) (string, bool) {
v.mu.Lock()
defer v.mu.Unlock()
addr = v.cleanIfLocal(addr)
_, ok := v.aliases[addr]
return addr, ok
v.mu.Unlock()
if ok {
return addr, true
}
return addr, v.runExistsHook(addr)
}
func (v *Resolver) resolve(rcount int, addr string) ([]Recipient, error) {
@@ -154,7 +169,18 @@ func (v *Resolver) resolve(rcount int, addr string) ([]Recipient, error) {
// match, which our callers can rely upon.
addr = v.cleanIfLocal(addr)
// Lookup in the aliases database.
v.mu.Lock()
rcpts := v.aliases[addr]
v.mu.Unlock()
// Augment with the hook results.
hr, err := v.runResolveHook(addr)
if err != nil {
return nil, err
}
rcpts = append(rcpts, hr...)
if len(rcpts) == 0 {
return []Recipient{{addr, EMAIL}}, nil
}
@@ -305,35 +331,43 @@ func parseReader(domain string, r io.Reader) (map[string][]Recipient, error) {
addr = addr + "@" + domain
addr, _ = normalize.Addr(addr)
if rawalias[0] == '|' {
cmd := strings.TrimSpace(rawalias[1:])
if cmd == "" {
// A pipe alias without a command is invalid.
continue
}
aliases[addr] = []Recipient{{cmd, PIPE}}
} else {
rs := []Recipient{}
for _, a := range strings.Split(rawalias, ",") {
a = strings.TrimSpace(a)
if a == "" {
continue
}
// Addresses with no domain get the current one added, so it's
// easier to share alias files.
if !strings.Contains(a, "@") {
a = a + "@" + domain
}
a, _ = normalize.Addr(a)
rs = append(rs, Recipient{a, EMAIL})
}
aliases[addr] = rs
}
rs := parseRHS(rawalias, domain)
aliases[addr] = rs
}
return aliases, scanner.Err()
}
func parseRHS(rawalias, domain string) []Recipient {
if len(rawalias) == 0 {
return nil
}
if rawalias[0] == '|' {
cmd := strings.TrimSpace(rawalias[1:])
if cmd == "" {
// A pipe alias without a command is invalid.
return nil
}
return []Recipient{{cmd, PIPE}}
}
rs := []Recipient{}
for _, a := range strings.Split(rawalias, ",") {
a = strings.TrimSpace(a)
if a == "" {
continue
}
// Addresses with no domain get the current one added, so it's
// easier to share alias files.
if !strings.Contains(a, "@") {
a = a + "@" + domain
}
a, _ = normalize.Addr(a)
rs = append(rs, Recipient{a, EMAIL})
}
return rs
}
// removeAllAfter removes everything from s that comes after the separators,
// including them.
func removeAllAfter(s, seps string) string {
@@ -361,3 +395,71 @@ func removeChars(s, chars string) string {
return s
}
func (v *Resolver) runResolveHook(addr string) ([]Recipient, error) {
if v.ResolveHook == "" {
hookResults.Add("resolve:notset", 1)
return nil, nil
}
// TODO: check if the file is executable.
if _, err := os.Stat(v.ResolveHook); os.IsNotExist(err) {
hookResults.Add("resolve:skip", 1)
return nil, nil
}
// TODO: this should be done via a context propagated all the way through.
tr := trace.New("Hook.Alias-Resolve", addr)
defer tr.Finish()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, v.ResolveHook, addr)
outb, err := cmd.Output()
out := string(outb)
tr.Debugf("stdout: %q", out)
if err != nil {
hookResults.Add("resolve:fail", 1)
tr.Error(err)
return nil, err
}
// Extract recipients from the output.
// Same format as the right hand side of aliases file, see parseRHS.
domain := envelope.DomainOf(addr)
raw := strings.TrimSpace(out)
rs := parseRHS(raw, domain)
tr.Debugf("recipients: %v", rs)
hookResults.Add("resolve:success", 1)
return rs, nil
}
func (v *Resolver) runExistsHook(addr string) bool {
if v.ExistsHook == "" {
hookResults.Add("exists:notset", 1)
return false
}
// TODO: check if the file is executable.
if _, err := os.Stat(v.ExistsHook); os.IsNotExist(err) {
hookResults.Add("exists:skip", 1)
return false
}
// TODO: this should be done via a context propagated all the way through.
tr := trace.New("Hook.Alias-Exists", addr)
defer tr.Finish()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, v.ExistsHook, addr)
err := cmd.Run()
if err != nil {
tr.Debugf("not exists: %v", err)
hookResults.Add("exists:false", 1)
return false
}
tr.Debugf("exists")
hookResults.Add("exists:true", 1)
return true
}