author | Alberto Bertogli
<albertito@blitiri.com.ar> 2016-09-21 23:22:39 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2016-10-09 23:51:04 UTC |
parent | bab8a8083c268359be8e7ff2602a272ef6084532 |
chasquid.go | +30 | -5 |
chasquid_test.go | +4 | -1 |
internal/aliases/aliases.go | +4 | -0 |
internal/config/config.go | +2 | -0 |
internal/config/config.pb.go | +30 | -17 |
internal/config/config.proto | +12 | -0 |
internal/queue/queue.go | +30 | -6 |
internal/queue/queue_test.go | +50 | -3 |
test/t-04-aliases/config/chasquid.conf | +11 | -0 |
test/t-04-aliases/config/domains/testserver/aliases | +12 | -0 |
test/t-04-aliases/content | +4 | -0 |
test/t-04-aliases/hosts | +1 | -0 |
test/t-04-aliases/msmtprc | +14 | -0 |
test/t-04-aliases/run.sh | +43 | -0 |
test/util/writemailto | +4 | -0 |
diff --git a/chasquid.go b/chasquid.go index 76decbf..d629438 100644 --- a/chasquid.go +++ b/chasquid.go @@ -19,6 +19,7 @@ import ( "syscall" "time" + "blitiri.com.ar/go/chasquid/internal/aliases" "blitiri.com.ar/go/chasquid/internal/auth" "blitiri.com.ar/go/chasquid/internal/config" "blitiri.com.ar/go/chasquid/internal/courier" @@ -80,6 +81,10 @@ func main() { s.Hostname = conf.Hostname s.MaxDataSize = conf.MaxDataSizeMb * 1024 * 1024 + aliasesR := aliases.NewResolver() + aliasesR.SuffixSep = conf.SuffixSeparators + aliasesR.DropChars = conf.DropCharacters + // Load domains. // They live inside the config directory, so the relative path works. domainDirs, err := ioutil.ReadDir("domains/") @@ -94,7 +99,7 @@ func main() { for _, info := range domainDirs { name := info.Name() dir := filepath.Join("domains", name) - loadDomain(s, name, dir) + loadDomain(name, dir, s, aliasesR) } } @@ -103,7 +108,7 @@ func main() { // as a remote domain (for loops, alias resolutions, etc.). s.AddDomain("localhost") - s.LoadQueue(conf.DataDir + "/queue") + s.InitQueue(conf.DataDir+"/queue", aliasesR) // Load the addresses and listeners. systemdLs, err := systemd.Listeners() @@ -139,9 +144,10 @@ func loadAddresses(srv *Server, addrs []string, ls []net.Listener, mode SocketMo } // Helper to load a single domain configuration into the server. -func loadDomain(s *Server, name, dir string) { +func loadDomain(name, dir string, s *Server, aliasesR *aliases.Resolver) { glog.Infof(" %s", name) s.AddDomain(name) + aliasesR.AddDomain(name) s.AddCerts(dir+"/cert.pem", dir+"/key.pem") if _, err := os.Stat(dir + "/users"); err == nil { @@ -154,6 +160,14 @@ func loadDomain(s *Server, name, dir string) { // TODO: periodically reload the database. } } + + if _, err := os.Stat(dir + "/aliases"); err == nil { + glog.Infof(" adding aliases") + err := aliasesR.AddAliasesFile(name, dir+"/aliases") + if err != nil { + glog.Errorf(" error: %v", err) + } + } } // Flush logs periodically, to help troubleshooting if there isn't that much @@ -251,13 +265,24 @@ func (s *Server) AddUserDB(domain string, db *userdb.DB) { s.userDBs[domain] = db } -func (s *Server) LoadQueue(path string) { - q := queue.New(path, s.localDomains) +func (s *Server) InitQueue(path string, aliasesR *aliases.Resolver) { + q := queue.New(path, s.localDomains, aliasesR) err := q.Load() if err != nil { glog.Fatalf("Error loading queue: %v", err) } s.queue = q + + // Launch the periodic reload of aliases, now that the queue may care + // about them. + go func() { + for range time.Tick(1 * time.Minute) { + err := aliasesR.Reload() + if err != nil { + glog.Errorf("Error reloading aliases: %v") + } + } + }() } func (s *Server) getTLSConfig() (*tls.Config, error) { diff --git a/chasquid_test.go b/chasquid_test.go index eda11bf..1641532 100644 --- a/chasquid_test.go +++ b/chasquid_test.go @@ -17,6 +17,7 @@ import ( "testing" "time" + "blitiri.com.ar/go/chasquid/internal/aliases" "blitiri.com.ar/go/chasquid/internal/userdb" "github.com/golang/glog" @@ -428,7 +429,9 @@ func realMain(m *testing.M) int { s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem") s.AddAddr(smtpAddr, ModeSMTP) s.AddAddr(submissionAddr, ModeSubmission) - s.LoadQueue(tmpDir + "/queue") + + ars := aliases.NewResolver() + s.InitQueue(tmpDir+"/queue", ars) udb := userdb.New("/dev/null") udb.AddUser("testuser", "testpasswd") diff --git a/internal/aliases/aliases.go b/internal/aliases/aliases.go index cafce0c..1b37fa1 100644 --- a/internal/aliases/aliases.go +++ b/internal/aliases/aliases.go @@ -191,6 +191,10 @@ func (v *Resolver) AddAliasesFile(domain, path string) error { return nil } +func (v *Resolver) AddAliasForTesting(addr, rcpt string, rType RType) { + v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, rType}) +} + func (v *Resolver) Reload() error { newAliases := map[string][]Recipient{} diff --git a/internal/config/config.go b/internal/config/config.go index e564f33..2635d72 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -75,4 +75,6 @@ func logConfig(c *Config) { glog.Infof(" Monitoring address: %s", c.MonitoringAddress) glog.Infof(" MDA: %s %v", c.MailDeliveryAgentBin, c.MailDeliveryAgentArgs) glog.Infof(" Data directory: %s", c.DataDir) + glog.Infof(" Suffix separators: %s", c.SuffixSeparators) + glog.Infof(" Drop characters: %s", c.DropCharacters) } diff --git a/internal/config/config.pb.go b/internal/config/config.pb.go index 0051e12..76976d8 100644 --- a/internal/config/config.pb.go +++ b/internal/config/config.pb.go @@ -62,6 +62,16 @@ type Config struct { // Directory where we store our persistent data. // Default: "/var/lib/chasquid" DataDir string `protobuf:"bytes,8,opt,name=data_dir,json=dataDir" json:"data_dir,omitempty"` + // Suffix separator, to perform suffix removal of local users. + // For example, if you set this to "-+", email to local user + // "user-blah" and "user+blah" will be delivered to "user". + // Default: none. + SuffixSeparators string `protobuf:"bytes,9,opt,name=suffix_separators,json=suffixSeparators" json:"suffix_separators,omitempty"` + // Characters to drop from the user part on local emails. + // For example, if you set this to "._", email to local user + // "u.se_r" will be delivered to "user". + // Default: none. + DropCharacters string `protobuf:"bytes,10,opt,name=drop_characters,json=dropCharacters" json:"drop_characters,omitempty"` } func (m *Config) Reset() { *m = Config{} } @@ -76,21 +86,24 @@ func init() { func init() { proto.RegisterFile("config.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 251 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0x41, 0x2f, 0x04, 0x31, - 0x14, 0xc7, 0xb3, 0x3b, 0xcc, 0x8e, 0x67, 0x25, 0xb6, 0x21, 0xca, 0x69, 0xb9, 0x70, 0xe1, 0x22, - 0xe2, 0x3c, 0xcc, 0xd5, 0x65, 0x7d, 0x80, 0xa6, 0xd5, 0x1a, 0x2f, 0xd9, 0xb6, 0x9b, 0xbe, 0x12, - 0x7c, 0x53, 0xdf, 0x46, 0x5b, 0xcc, 0x4a, 0x38, 0xbe, 0xff, 0xef, 0xf7, 0x6f, 0x5f, 0x1e, 0x4c, - 0x1f, 0xbc, 0x7b, 0xc4, 0xfe, 0x62, 0x15, 0x7c, 0xf4, 0x27, 0x1f, 0x63, 0xa8, 0x6f, 0x4b, 0xc0, - 0x8e, 0xa0, 0x79, 0xf2, 0x14, 0x9d, 0xb4, 0x86, 0x8f, 0xe6, 0xa3, 0xb3, 0xad, 0xc5, 0x30, 0xb3, - 0x53, 0xd8, 0xb5, 0xf2, 0x55, 0x68, 0x19, 0xa5, 0x20, 0x7c, 0x37, 0xc2, 0x2a, 0x3e, 0x4e, 0x4e, - 0xb5, 0xd8, 0x49, 0x79, 0x97, 0xe2, 0xfb, 0x94, 0xde, 0x29, 0x76, 0x0c, 0x53, 0xb2, 0x71, 0x25, - 0xa4, 0xd6, 0xc1, 0x10, 0xf1, 0x6a, 0x5e, 0xa5, 0x87, 0xb6, 0x73, 0xd6, 0x7e, 0x45, 0xec, 0x1c, - 0x18, 0x3d, 0x2b, 0x8b, 0x44, 0xe8, 0xdd, 0x20, 0x6e, 0x14, 0x71, 0xb6, 0x26, 0xbf, 0x74, 0xeb, - 0x1d, 0x46, 0x1f, 0xd0, 0xf5, 0x83, 0xbe, 0x59, 0x16, 0x9c, 0xad, 0xc9, 0x8f, 0x7e, 0x05, 0x07, - 0x56, 0xe2, 0x52, 0x68, 0xb3, 0xc4, 0x17, 0x13, 0xde, 0x84, 0xec, 0x8d, 0x8b, 0x42, 0xa1, 0xe3, - 0x75, 0xe9, 0xec, 0x65, 0xdc, 0x7d, 0xd3, 0x36, 0xc3, 0x1b, 0x74, 0xec, 0x1a, 0xf8, 0x7f, 0x35, - 0x19, 0x7a, 0xe2, 0x93, 0xb2, 0xda, 0xfe, 0x9f, 0x5e, 0x9b, 0x20, 0x3b, 0x84, 0xa6, 0x5c, 0x45, - 0x63, 0xe0, 0x4d, 0xf9, 0x60, 0x92, 0xe7, 0x0e, 0x83, 0xaa, 0xcb, 0x89, 0x2f, 0x3f, 0x03, 0x00, - 0x00, 0xff, 0xff, 0xc5, 0x2e, 0x79, 0xef, 0x72, 0x01, 0x00, 0x00, + // 296 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x91, 0x41, 0x4e, 0xeb, 0x30, + 0x10, 0x86, 0xd5, 0x97, 0x47, 0x9a, 0x9a, 0x02, 0xad, 0x05, 0xc2, 0xb0, 0x2a, 0x6c, 0x40, 0x42, + 0xb0, 0x41, 0x88, 0x75, 0x68, 0xb6, 0x6c, 0xda, 0x03, 0x58, 0x4e, 0xec, 0xa4, 0x23, 0xd5, 0x76, + 0x64, 0x1b, 0x54, 0xb8, 0x14, 0x57, 0x64, 0xe2, 0x42, 0x8a, 0x04, 0xcb, 0xf9, 0xfe, 0x6f, 0xec, + 0xf1, 0x98, 0x8c, 0x2b, 0x6b, 0x6a, 0x68, 0xee, 0x5a, 0x67, 0x83, 0xbd, 0xfc, 0x48, 0x48, 0x3a, + 0x8f, 0x80, 0x9e, 0x93, 0x6c, 0x65, 0x7d, 0x30, 0x42, 0x2b, 0x36, 0x98, 0x0d, 0xae, 0x47, 0x8b, + 0xbe, 0xa6, 0x57, 0x64, 0xa2, 0xc5, 0x86, 0x4b, 0x11, 0x04, 0xf7, 0xf0, 0xae, 0xb8, 0x2e, 0xd9, + 0x3f, 0x74, 0x92, 0xc5, 0x01, 0xf2, 0x02, 0xf1, 0x12, 0xe9, 0x73, 0x49, 0x2f, 0xc8, 0xd8, 0xeb, + 0xd0, 0x72, 0x21, 0xa5, 0x53, 0xde, 0xb3, 0x64, 0x96, 0xe0, 0x41, 0xfb, 0x1d, 0xcb, 0xb7, 0x88, + 0xde, 0x12, 0xea, 0x5f, 0x4a, 0x0d, 0xde, 0x83, 0x35, 0xbd, 0xf8, 0x3f, 0x8a, 0xd3, 0x5d, 0xf2, + 0x43, 0xd7, 0xd6, 0x40, 0xb0, 0x0e, 0x4c, 0xd3, 0xeb, 0x7b, 0x71, 0xc0, 0xe9, 0x2e, 0xf9, 0xd6, + 0x1f, 0xc8, 0xa9, 0x16, 0xb0, 0xe6, 0x52, 0xad, 0xe1, 0x55, 0xb9, 0x37, 0x2e, 0x1a, 0x65, 0x02, + 0x2f, 0xc1, 0xb0, 0x34, 0xf6, 0x1c, 0x77, 0x71, 0xf1, 0x95, 0xe6, 0x5d, 0xf8, 0x04, 0x86, 0x3e, + 0x12, 0xf6, 0x57, 0x9b, 0x70, 0x8d, 0x67, 0xc3, 0x38, 0xda, 0xc9, 0xaf, 0xbe, 0x1c, 0x43, 0x7a, + 0x46, 0xb2, 0xb8, 0x15, 0x09, 0x8e, 0x65, 0xf1, 0x82, 0x61, 0x57, 0x17, 0xe0, 0xe8, 0x0d, 0xc1, + 0xe7, 0xd4, 0x35, 0x6c, 0xb8, 0x57, 0xad, 0x70, 0x02, 0x07, 0xf5, 0x6c, 0x14, 0x9d, 0xc9, 0x36, + 0x58, 0xf6, 0x1c, 0x37, 0x7c, 0x24, 0x9d, 0x6d, 0x79, 0xb5, 0x42, 0x52, 0x05, 0x85, 0x2a, 0x89, + 0xea, 0x61, 0x87, 0xe7, 0x3d, 0x2d, 0xd3, 0xf8, 0x71, 0xf7, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, + 0xd1, 0x68, 0xa2, 0x22, 0xc8, 0x01, 0x00, 0x00, } diff --git a/internal/config/config.proto b/internal/config/config.proto index 03512a7..b68c159 100644 --- a/internal/config/config.proto +++ b/internal/config/config.proto @@ -42,5 +42,17 @@ message Config { // Directory where we store our persistent data. // Default: "/var/lib/chasquid" string data_dir = 8; + + // Suffix separator, to perform suffix removal of local users. + // For example, if you set this to "-+", email to local user + // "user-blah" and "user+blah" will be delivered to "user". + // Default: none. + string suffix_separators = 9; + + // Characters to drop from the user part on local emails. + // For example, if you set this to "._", email to local user + // "u.se_r" will be delivered to "user". + // Default: none. + string drop_characters = 10; } diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 0655801..c079f25 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -19,6 +19,7 @@ import ( "bytes" + "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/protoio" @@ -91,10 +92,13 @@ type Queue struct { // Path where we store the queue. path string + + // Aliases resolver. + aliases *aliases.Resolver } // Load the queue and launch the sending loops on startup. -func New(path string, localDomains *set.String) *Queue { +func New(path string, localDomains *set.String, aliases *aliases.Resolver) *Queue { os.MkdirAll(path, 0700) return &Queue{ @@ -103,6 +107,7 @@ func New(path string, localDomains *set.String) *Queue { remoteC: &courier.SMTP{}, localDomains: localDomains, path: path, + aliases: aliases, } } @@ -151,11 +156,30 @@ func (q *Queue) Put(from string, to []string, data []byte) (string, error) { } for _, t := range to { - item.Rcpt = append(item.Rcpt, &Recipient{ - Address: t, - Type: Recipient_EMAIL, - Status: Recipient_PENDING, - }) + rcpts, err := q.aliases.Resolve(t) + if err != nil { + return "", fmt.Errorf("error resolving aliases for %q: %v", t, err) + } + + // Add the recipients (after resolving aliases); this conversion is + // not very pretty but at least it's self contained. + for _, aliasRcpt := range rcpts { + r := &Recipient{ + Address: aliasRcpt.Addr, + Status: Recipient_PENDING, + } + switch aliasRcpt.Type { + case aliases.EMAIL: + r.Type = Recipient_EMAIL + case aliases.PIPE: + r.Type = Recipient_PIPE + default: + glog.Errorf("unknown alias type %v when resolving %q", + aliasRcpt.Type, t) + return "", fmt.Errorf("internal error - unknown alias type") + } + item.Rcpt = append(item.Rcpt, r) + } } err := item.WriteTo(q.path) diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go index 26e3d51..baf18ce 100644 --- a/internal/queue/queue_test.go +++ b/internal/queue/queue_test.go @@ -3,10 +3,12 @@ package queue import ( "bytes" "fmt" + "reflect" "sync" "testing" "time" + "blitiri.com.ar/go/chasquid/internal/aliases" "blitiri.com.ar/go/chasquid/internal/set" ) @@ -58,7 +60,7 @@ func newTestCourier() *TestCourier { func TestBasic(t *testing.T) { localC := newTestCourier() remoteC := newTestCourier() - q := New("/tmp/queue_test", set.NewString("loco")) + q := New("/tmp/queue_test", set.NewString("loco"), aliases.NewResolver()) q.localC = localC q.remoteC = remoteC @@ -99,7 +101,7 @@ func TestBasic(t *testing.T) { } func TestFullQueue(t *testing.T) { - q := New("/tmp/queue_test", set.NewString()) + q := New("/tmp/queue_test", set.NewString(), aliases.NewResolver()) // Force-insert maxQueueSize items in the queue. oneID := "" @@ -136,8 +138,53 @@ func TestFullQueue(t *testing.T) { q.Remove(id) } +// Dumb courier, for when we don't care for the results. +type DumbCourier struct{} + +func (c DumbCourier) Deliver(from string, to string, data []byte) error { + return nil +} + +func TestAliases(t *testing.T) { + q := New("/tmp/queue_test", set.NewString("loco"), aliases.NewResolver()) + q.localC = DumbCourier{} + q.remoteC = DumbCourier{} + + q.aliases.AddDomain("loco") + q.aliases.AddAliasForTesting("ab@loco", "pq@loco", aliases.EMAIL) + q.aliases.AddAliasForTesting("ab@loco", "rs@loco", aliases.EMAIL) + q.aliases.AddAliasForTesting("ab@loco", "command", aliases.PIPE) + q.aliases.AddAliasForTesting("cd@loco", "ata@hualpa", aliases.EMAIL) + + cases := []struct { + to []string + expected []*Recipient + }{ + {[]string{"ab@loco"}, []*Recipient{ + {"pq@loco", Recipient_EMAIL, Recipient_PENDING}, + {"rs@loco", Recipient_EMAIL, Recipient_PENDING}, + {"command", Recipient_PIPE, Recipient_PENDING}}}, + {[]string{"ab@loco", "cd@loco"}, []*Recipient{ + {"pq@loco", Recipient_EMAIL, Recipient_PENDING}, + {"rs@loco", Recipient_EMAIL, Recipient_PENDING}, + {"command", Recipient_PIPE, Recipient_PENDING}, + {"ata@hualpa", Recipient_EMAIL, Recipient_PENDING}}}, + } + for _, c := range cases { + id, err := q.Put("from", c.to, []byte("data")) + if err != nil { + t.Errorf("Put: %v", err) + } + item := q.q[id] + if !reflect.DeepEqual(item.Rcpt, c.expected) { + t.Errorf("case %q, expected %v, got %v", c.to, item.Rcpt, c.expected) + } + q.Remove(id) + } +} + func TestPipes(t *testing.T) { - q := New("/tmp/queue_test", set.NewString("loco")) + q := New("/tmp/queue_test", set.NewString("loco"), aliases.NewResolver()) item := &Item{ Message: Message{ ID: <-newID, diff --git a/test/t-04-aliases/config/chasquid.conf b/test/t-04-aliases/config/chasquid.conf new file mode 100644 index 0000000..0d0b455 --- /dev/null +++ b/test/t-04-aliases/config/chasquid.conf @@ -0,0 +1,11 @@ +smtp_address: ":1025" +submission_address: ":1587" +monitoring_address: ":1099" + +mail_delivery_agent_bin: "test-mda" +mail_delivery_agent_args: "%user%@%domain%" + +data_dir: "../.data" + +suffix_separators: "+-" +drop_characters: "._" diff --git a/test/t-04-aliases/config/domains/testserver/aliases b/test/t-04-aliases/config/domains/testserver/aliases new file mode 100644 index 0000000..0ab5f52 --- /dev/null +++ b/test/t-04-aliases/config/domains/testserver/aliases @@ -0,0 +1,12 @@ + +# Easy aliases. +pepe: jose +joan: juan + +# UTF-8 aliases. +pitanga: ñangapirí +añil: azul, índigo + +# Pipe aliases. +tubo: | writemailto ../.data/pipe_alias_worked + diff --git a/test/t-04-aliases/content b/test/t-04-aliases/content new file mode 100644 index 0000000..76a8b16 --- /dev/null +++ b/test/t-04-aliases/content @@ -0,0 +1,4 @@ +Subject: Prueba desde el test + +Crece desde el test el futuro +Crece desde el test diff --git a/test/t-04-aliases/hosts b/test/t-04-aliases/hosts new file mode 100644 index 0000000..2b9b623 --- /dev/null +++ b/test/t-04-aliases/hosts @@ -0,0 +1 @@ +testserver localhost diff --git a/test/t-04-aliases/msmtprc b/test/t-04-aliases/msmtprc new file mode 100644 index 0000000..1679764 --- /dev/null +++ b/test/t-04-aliases/msmtprc @@ -0,0 +1,14 @@ +account default + +host testserver +port 1587 + +tls on +tls_trust_file config/domains/testserver/cert.pem + +from user@testserver + +auth on +user user@testserver +password secretpassword + diff --git a/test/t-04-aliases/run.sh b/test/t-04-aliases/run.sh new file mode 100755 index 0000000..5128b81 --- /dev/null +++ b/test/t-04-aliases/run.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e +. $(dirname ${0})/../util/lib.sh + +init + +generate_certs_for testserver +add_user testserver user secretpassword + +mkdir -p .logs +chasquid -v=2 --log_dir=.logs --config_dir=config & +wait_until_ready 1025 + +function send_and_check() { + run_msmtp $1@testserver < content + shift + for i in $@; do + wait_for_file .mail/$i@testserver + mail_diff content .mail/$i@testserver + rm -f .mail/$i@testserver + done +} + +# Test email aliases. +send_and_check pepe jose +send_and_check joan juan +send_and_check pitanga ñangapirí +send_and_check añil azul índigo + +# Test suffix separators and drop characters. +send_and_check a.ñi_l azul índigo +send_and_check añil-blah azul índigo +send_and_check añil+blah azul índigo + +# Test the pipe alias separately. +rm -f .data/pipe_alias_worked +run_msmtp tubo@testserver < content +wait_for_file .data/pipe_alias_worked +mail_diff content .data/pipe_alias_worked + + +success diff --git a/test/util/writemailto b/test/util/writemailto new file mode 100755 index 0000000..63d3d33 --- /dev/null +++ b/test/util/writemailto @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "From writemailto" > "$1" +exec cat >> "$1"