git » chasquid » commit ff103c1

courier: Let the users configure the mail delivery agent

author Alberto Bertogli
2016-07-16 11:29:58 UTC
committer Alberto Bertogli
2016-07-16 11:33:51 UTC
parent bb08be40231c32cbd9f9d1193751352a17896ee3

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 -0
internal/config/config.go +9 -0
internal/config/config.pb.go +43 -3
internal/config/config.proto +14 -0
internal/courier/procmail.go +6 -4
internal/courier/procmail_test.go +11 -9
internal/userdb/userdb.go +12 -1
internal/userdb/userdb_test.go +21 -1

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 (*Config) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
+
+func init() {
+	proto.RegisterType((*Config)(nil), "Config")
 }
 
-func (m *Config) Reset()         { *m = Config{} }
-func (m *Config) String() string { return proto.CompactTextString(m) }
-func (*Config) ProtoMessage()    {}
+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)