From ff103c18c3bcffc88ac2ce9829c8ccedb81a86d8 Mon Sep 17 00:00:00 2001 From: Alberto Bertogli Date: Sat, 16 Jul 2016 12:29:58 +0100 Subject: [PATCH] courier: Let the users configure the mail delivery agent This patch adds configuration options for the MDA binary and command line arguments, and changes the (soon to be renamed) procmail courier to make use of them. --- chasquid.go | 3 ++ internal/config/config.go | 9 ++++++ internal/config/config.pb.go | 46 +++++++++++++++++++++++++++++-- internal/config/config.proto | 14 ++++++++++ internal/courier/procmail.go | 10 ++++--- internal/courier/procmail_test.go | 20 ++++++++------ internal/userdb/userdb.go | 13 ++++++++- internal/userdb/userdb_test.go | 22 ++++++++++++++- 8 files changed, 119 insertions(+), 18 deletions(-) diff --git a/chasquid.go b/chasquid.go index 9beacf1..5dd5721 100644 --- a/chasquid.go +++ b/chasquid.go @@ -51,6 +51,9 @@ func main() { go http.ListenAndServe(conf.MonitoringAddress, nil) } + courier.MailDeliveryAgentBin = conf.MailDeliveryAgentBin + courier.MailDeliveryAgentArgs = conf.MailDeliveryAgentArgs + s := NewServer() s.Hostname = conf.Hostname s.MaxDataSize = conf.MaxDataSizeMb * 1024 * 1024 diff --git a/internal/config/config.go b/internal/config/config.go index 3f8c3e2..0b48a69 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,6 +47,14 @@ func Load(path string) (*Config, error) { c.Address = append(c.Address, "systemd") } + if c.MailDeliveryAgentBin == "" { + c.MailDeliveryAgentBin = "procmail" + } + if len(c.MailDeliveryAgentArgs) == 0 { + c.MailDeliveryAgentArgs = append(c.MailDeliveryAgentArgs, + "-d", "%user%") + } + logConfig(c) return c, nil } @@ -57,4 +65,5 @@ func logConfig(c *Config) { glog.Infof(" Max data size (MB): %d", c.MaxDataSizeMb) glog.Infof(" Addresses: %v", c.Address) glog.Infof(" Monitoring address: %s", c.MonitoringAddress) + glog.Infof(" MDA: %s %v", c.MailDeliveryAgentBin, c.MailDeliveryAgentArgs) } diff --git a/internal/config/config.pb.go b/internal/config/config.pb.go index ddad207..ba208c0 100644 --- a/internal/config/config.pb.go +++ b/internal/config/config.pb.go @@ -22,6 +22,12 @@ var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + type Config struct { // Hostname to use when we say hello. // For aesthetic purposes, but may help if our ip address resolves to it. @@ -36,8 +42,42 @@ type Config struct { // Address for the monitoring http server. // Default: no monitoring http server. MonitoringAddress string `protobuf:"bytes,4,opt,name=monitoring_address" json:"monitoring_address,omitempty"` + // Mail delivery agent (MDA, also known as LDA) to use. + // This should point to the binary to use to deliver email to local users. + // The content of the email will be passed via stdin. + // If it exits unsuccessfully, we assume the mail was not delivered. + // Default: "procmail". + MailDeliveryAgentBin string `protobuf:"bytes,5,opt,name=mail_delivery_agent_bin" json:"mail_delivery_agent_bin,omitempty"` + // Command line arguments for the mail delivery agent. One per argument. + // Some replacements will be done: + // - "%user%" -> local user (anything before the @) + // - "%domain%" -> domain (anything after the @) + // Default: "-d", "%user" (adequate for procmail) + MailDeliveryAgentArgs []string `protobuf:"bytes,6,rep,name=mail_delivery_agent_args" json:"mail_delivery_agent_args,omitempty"` } -func (m *Config) Reset() { *m = Config{} } -func (m *Config) String() string { return proto.CompactTextString(m) } -func (*Config) ProtoMessage() {} +func (m *Config) Reset() { *m = Config{} } +func (m *Config) String() string { return proto.CompactTextString(m) } +func (*Config) ProtoMessage() {} +func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func init() { + proto.RegisterType((*Config)(nil), "Config") +} + +func init() { proto.RegisterFile("config.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 169 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x8e, 0x41, 0xca, 0xc2, 0x30, + 0x10, 0x46, 0xe9, 0xdf, 0xdf, 0xaa, 0x83, 0x60, 0xc9, 0xc6, 0xc1, 0x8d, 0xc5, 0x95, 0x2b, 0x37, + 0x1e, 0xc1, 0x83, 0x0c, 0x53, 0x13, 0xe3, 0x40, 0x93, 0x48, 0x12, 0x44, 0x3d, 0x8f, 0x07, 0xb5, + 0x06, 0xdc, 0xb9, 0xfd, 0xde, 0xe3, 0xf1, 0xc1, 0xe2, 0x14, 0xfc, 0x59, 0xec, 0xfe, 0x1a, 0x43, + 0x0e, 0xdb, 0x57, 0x05, 0xcd, 0xb1, 0x0c, 0xaa, 0x85, 0xd9, 0x25, 0xa4, 0xec, 0xd9, 0x19, 0xac, + 0xba, 0x6a, 0x37, 0x57, 0x08, 0xad, 0xe3, 0x3b, 0x69, 0xce, 0x4c, 0x49, 0x9e, 0x86, 0x5c, 0x8f, + 0x7f, 0x23, 0xa9, 0xd5, 0x12, 0xa6, 0xac, 0x75, 0x34, 0x29, 0x61, 0xdd, 0xd5, 0xa3, 0xba, 0x06, + 0xe5, 0x82, 0x97, 0x1c, 0xa2, 0x78, 0x4b, 0x5f, 0xf6, 0x5f, 0x32, 0x1b, 0x58, 0x39, 0x96, 0x81, + 0xb4, 0x19, 0xe4, 0x66, 0xe2, 0x83, 0xd8, 0x1a, 0x9f, 0xa9, 0x17, 0x8f, 0x93, 0x22, 0x74, 0x80, + 0xbf, 0x04, 0x8e, 0x36, 0x61, 0xf3, 0xc9, 0xf7, 0x4d, 0x79, 0x7b, 0x78, 0x07, 0x00, 0x00, 0xff, + 0xff, 0xa3, 0xe4, 0x58, 0xd3, 0xbd, 0x00, 0x00, 0x00, +} diff --git a/internal/config/config.proto b/internal/config/config.proto index d87682c..507c06d 100644 --- a/internal/config/config.proto +++ b/internal/config/config.proto @@ -18,5 +18,19 @@ message Config { // Address for the monitoring http server. // Default: no monitoring http server. string monitoring_address = 4; + + // Mail delivery agent (MDA, also known as LDA) to use. + // This should point to the binary to use to deliver email to local users. + // The content of the email will be passed via stdin. + // If it exits unsuccessfully, we assume the mail was not delivered. + // Default: "procmail". + string mail_delivery_agent_bin = 5; + + // Command line arguments for the mail delivery agent. One per argument. + // Some replacements will be done: + // - "%user%" -> local user (anything before the @) + // - "%domain%" -> domain (anything after the @) + // Default: "-d", "%user" (adequate for procmail) + repeated string mail_delivery_agent_args = 6; } diff --git a/internal/courier/procmail.go b/internal/courier/procmail.go index 06c3a52..c307500 100644 --- a/internal/courier/procmail.go +++ b/internal/courier/procmail.go @@ -14,8 +14,10 @@ import ( var ( // Location of the procmail binary, and arguments to use. // The string "%user%" will be replaced with the local user. - procmailBin = "procmail" - procmailArgs = []string{"-d", "%user%"} + // TODO: Make these a part of the courier instance itself? Why do they + // have to be global? + MailDeliveryAgentBin = "procmail" + MailDeliveryAgentArgs = []string{"-d", "%user%"} // Give procmail 1m to deliver mail. procmailTimeout = 1 * time.Minute @@ -39,10 +41,10 @@ func (p *Procmail) Deliver(from string, to string, data []byte) error { // Prepare the command, replacing the necessary arguments. args := []string{} - for _, a := range procmailArgs { + for _, a := range MailDeliveryAgentArgs { args = append(args, strings.Replace(a, "%user%", user, -1)) } - cmd := exec.Command(procmailBin, args...) + cmd := exec.Command(MailDeliveryAgentBin, args...) cmdStdin, err := cmd.StdinPipe() if err != nil { diff --git a/internal/courier/procmail_test.go b/internal/courier/procmail_test.go index f700a18..c806acc 100644 --- a/internal/courier/procmail_test.go +++ b/internal/courier/procmail_test.go @@ -15,8 +15,8 @@ func TestProcmail(t *testing.T) { } defer os.RemoveAll(dir) - procmailBin = "tee" - procmailArgs = []string{dir + "/%user%"} + MailDeliveryAgentBin = "tee" + MailDeliveryAgentArgs = []string{dir + "/%user%"} p := Procmail{} err = p.Deliver("from@x", "to@y", []byte("data")) @@ -31,8 +31,8 @@ func TestProcmail(t *testing.T) { } func TestProcmailTimeout(t *testing.T) { - procmailBin = "/bin/sleep" - procmailArgs = []string{"1"} + MailDeliveryAgentBin = "/bin/sleep" + MailDeliveryAgentArgs = []string{"1"} procmailTimeout = 100 * time.Millisecond p := Procmail{} @@ -48,19 +48,21 @@ func TestProcmailBadCommandLine(t *testing.T) { p := Procmail{} // Non-existent binary. - procmailBin = "thisdoesnotexist" + MailDeliveryAgentBin = "thisdoesnotexist" err := p.Deliver("from", "to", []byte("data")) if err == nil { - t.Errorf("Unexpected success: %q %v", procmailBin, procmailArgs) + t.Errorf("Unexpected success: %q %v", + MailDeliveryAgentBin, MailDeliveryAgentArgs) } // Incorrect arguments. - procmailBin = "cat" - procmailArgs = []string{"--fail_unknown_option"} + MailDeliveryAgentBin = "cat" + MailDeliveryAgentArgs = []string{"--fail_unknown_option"} err = p.Deliver("from", "to", []byte("data")) if err == nil { - t.Errorf("Unexpected success: %q %v", procmailBin, procmailArgs) + t.Errorf("Unexpected success: %q %v", + MailDeliveryAgentBin, MailDeliveryAgentArgs) } } diff --git a/internal/userdb/userdb.go b/internal/userdb/userdb.go index 46ca213..d73f0e8 100644 --- a/internal/userdb/userdb.go +++ b/internal/userdb/userdb.go @@ -79,6 +79,13 @@ var ( InvalidUsernameErr = errors.New("username contains invalid characters") ) +func New(fname string) *DB { + return &DB{ + fname: fname, + users: map[string]user{}, + } +} + // Load the database from the given file. // Return the database, a list of warnings (if any), and a fatal error if the // database could not be loaded. @@ -194,7 +201,11 @@ func (db *DB) Write() error { base64.StdEncoding.EncodeToString([]byte(user.password))) } - return safeio.WriteFile(db.fname, buf.Bytes(), db.finfo.Mode()) + mode := os.FileMode(0660) + if db.finfo != nil { + mode = db.finfo.Mode() + } + return safeio.WriteFile(db.fname, buf.Bytes(), mode) } // Does this user exist in the database? diff --git a/internal/userdb/userdb_test.go b/internal/userdb/userdb_test.go index b5cfe3c..e3a1ef2 100644 --- a/internal/userdb/userdb_test.go +++ b/internal/userdb/userdb_test.go @@ -1,6 +1,7 @@ package userdb import ( + "fmt" "io/ioutil" "os" "testing" @@ -41,7 +42,7 @@ func dbEquals(a, b *DB) bool { for k, av := range a.users { bv, ok := b.users[k] - if !ok || av != bv { + if !ok || av.name != bv.name || av.password != bv.password { return false } } @@ -203,6 +204,25 @@ func TestWrite(t *testing.T) { } } +func TestNew(t *testing.T) { + fname := fmt.Sprintf("%s/userdb_test-%d", os.TempDir(), os.Getpid()) + db1 := New(fname) + db1.AddUser("user", "passwd") + db1.Write() + + db2, ws, err := Load(fname) + if err != nil { + t.Fatalf("error loading: %v", err) + } + if len(ws) != 0 { + t.Errorf("warnings loading: %v", ws) + } + + if !dbEquals(db1, db2) { + t.Errorf("databases differ. db1:%v != db2:%v", db1, db2) + } +} + func TestInvalidUsername(t *testing.T) { fname := mustCreateDB(t, "") defer removeIfSuccessful(t, fname)