From 7cac94f0f3e6d31b6b164dc223de36eb94e33256 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 10:06:30 +0900 Subject: [PATCH 01/39] Implement some parts for loading data from a directory --- aliasdomain.go | 34 ++++ aliasuser.go | 48 ++++++ catchalluser.go | 24 +++ const.go | 13 ++ domain.go | 27 +++ repository.go | 399 ++++++++++++++++++++++++++++++++++++++++++++ repositoryconfig.go | 214 ++++++++++++++++++++++++ user.go | 38 +++++ valid.go | 52 ++++++ version.go | 4 + 10 files changed, 853 insertions(+) create mode 100644 aliasdomain.go create mode 100644 aliasuser.go create mode 100644 catchalluser.go create mode 100644 const.go create mode 100644 domain.go create mode 100644 repository.go create mode 100644 repositoryconfig.go create mode 100644 user.go create mode 100644 valid.go create mode 100644 version.go diff --git a/aliasdomain.go b/aliasdomain.go new file mode 100644 index 0000000..165ebf2 --- /dev/null +++ b/aliasdomain.go @@ -0,0 +1,34 @@ +package mailfull + +// AliasDomain represents a AliasDomain. +type AliasDomain struct { + name string + target string +} + +// NewAliasDomain creates a new AliasDomain instance. +func NewAliasDomain(name, target string) (*AliasDomain, error) { + if !validAliasDomainName(name) { + return nil, ErrInvalidAliasDomainName + } + if !validAliasDomainTarget(target) { + return nil, ErrInvalidAliasDomainTarget + } + + ad := &AliasDomain{ + name: name, + target: target, + } + + return ad, nil +} + +// Name returns name. +func (ad *AliasDomain) Name() string { + return ad.name +} + +// Target returns target. +func (ad *AliasDomain) Target() string { + return ad.target +} diff --git a/aliasuser.go b/aliasuser.go new file mode 100644 index 0000000..7b04696 --- /dev/null +++ b/aliasuser.go @@ -0,0 +1,48 @@ +package mailfull + +import "errors" + +// Errors for parameter. +var ( + ErrNotEnoughAliasUserTargets = errors.New("AliasUser: targets not enough") +) + +// AliasUser represents a AliasUser. +type AliasUser struct { + name string + targets []string +} + +// NewAliasUser creates a new AliasUser instance. +func NewAliasUser(name string, targets []string) (*AliasUser, error) { + if !validAliasUserName(name) { + return nil, ErrInvalidAliasUserName + } + + if len(targets) < 1 { + return nil, ErrNotEnoughAliasUserTargets + } + + for _, target := range targets { + if !validAliasUserTarget(target) { + return nil, ErrInvalidAliasUserTarget + } + } + + au := &AliasUser{ + name: name, + targets: targets, + } + + return au, nil +} + +// Name returns name. +func (au *AliasUser) Name() string { + return au.name +} + +// Targets returns targets. +func (au *AliasUser) Targets() []string { + return au.targets +} diff --git a/catchalluser.go b/catchalluser.go new file mode 100644 index 0000000..61e93bf --- /dev/null +++ b/catchalluser.go @@ -0,0 +1,24 @@ +package mailfull + +// CatchAllUser represents a CatchAllUser. +type CatchAllUser struct { + name string +} + +// NewCatchAllUser creates a new CatchAllUser instance. +func NewCatchAllUser(name string) (*CatchAllUser, error) { + if !validCatchAllUserName(name) { + return nil, ErrInvalidCatchAllUserName + } + + cu := &CatchAllUser{ + name: name, + } + + return cu, nil +} + +// Name returns name. +func (cu *CatchAllUser) Name() string { + return cu.name +} diff --git a/const.go b/const.go new file mode 100644 index 0000000..98164a8 --- /dev/null +++ b/const.go @@ -0,0 +1,13 @@ +package mailfull + +// Filenames that are contained in the Repository. +const ( + DirNameConfig = ".mailfull" + FileNameConfig = "config" + + FileNameAliasDomains = ".valiasdomains" + FileNameUsersPassword = ".vpasswd" + FileNameUserForwards = ".forward" + FileNameAliasUsers = ".valiases" + FileNameCatchAllUser = ".vcatchall" +) diff --git a/domain.go b/domain.go new file mode 100644 index 0000000..1ca5d37 --- /dev/null +++ b/domain.go @@ -0,0 +1,27 @@ +package mailfull + +// Domain represents a Domain. +type Domain struct { + name string + Users []*User + AliasUsers []*AliasUser + CatchAllUser *CatchAllUser +} + +// NewDomain creates a new Domain instance. +func NewDomain(name string) (*Domain, error) { + if !validDomainName(name) { + return nil, ErrInvalidDomainName + } + + d := &Domain{ + name: name, + } + + return d, nil +} + +// Name returns name. +func (d *Domain) Name() string { + return d.name +} diff --git a/repository.go b/repository.go new file mode 100644 index 0000000..0763019 --- /dev/null +++ b/repository.go @@ -0,0 +1,399 @@ +package mailfull + +import ( + "bufio" + "errors" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" +) + +// Errors for the operation of the Repository. +var ( + ErrDomainNotExist = errors.New("Domain: not exist") + ErrUserNotExist = errors.New("User: not exist") + + ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") + ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") + ErrInvalidFormatAliasUsers = errors.New("AliasUsers: file invalid format") +) + +// Repository represents a Repository. +type Repository struct { + *RepositoryConfig +} + +// NewRepository creates a new Repository instance. +func NewRepository(c *RepositoryConfig) (*Repository, error) { + r := &Repository{ + RepositoryConfig: c, + } + + return r, nil +} + +// Domains returns a Domain slice. +func (r *Repository) Domains() ([]*Domain, error) { + fileInfos, err := ioutil.ReadDir(r.DirMailDataPath) + if err != nil { + return nil, err + } + + domains := make([]*Domain, 0, len(fileInfos)) + + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + continue + } + + name := fileInfo.Name() + + domain, err := NewDomain(name) + if err != nil { + continue + } + + domains = append(domains, domain) + } + + return domains, nil +} + +// Domain returns a Domain of the input name. +func (r *Repository) Domain(domainName string) (*Domain, error) { + if !validDomainName(domainName) { + return nil, ErrInvalidDomainName + } + + fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName)) + if err != nil { + return nil, err + } + + if !fileInfo.IsDir() { + return nil, nil + } + + name := domainName + + domain, err := NewDomain(name) + if err != nil { + return nil, err + } + + return domain, nil +} + +// AliasDomains returns a AliasDomain slice. +func (r *Repository) AliasDomains() ([]*AliasDomain, error) { + file, err := os.Open(filepath.Join(r.DirMailDataPath, FileNameAliasDomains)) + if err != nil { + return nil, err + } + + aliasDomains := make([]*AliasDomain, 0, 10) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + words := strings.Split(scanner.Text(), ":") + if len(words) != 2 { + return nil, ErrInvalidFormatAliasDomain + } + + name := words[0] + target := words[1] + + aliasDomain, err := NewAliasDomain(name, target) + if err != nil { + return nil, err + } + + aliasDomains = append(aliasDomains, aliasDomain) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return aliasDomains, nil +} + +// AliasDomain returns a AliasDomain of the input name. +func (r *Repository) AliasDomain(aliasDomainName string) (*AliasDomain, error) { + aliasDomains, err := r.AliasDomains() + if err != nil { + return nil, err + } + + for _, aliasDomain := range aliasDomains { + if aliasDomain.Name() == aliasDomainName { + return aliasDomain, nil + } + } + + return nil, nil +} + +// Users returns a User slice. +func (r *Repository) Users(domainName string) ([]*User, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + hashedPasswords, err := r.usersHashedPassword(domainName) + if err != nil { + return nil, err + } + + fileInfos, err := ioutil.ReadDir(filepath.Join(r.DirMailDataPath, domainName)) + if err != nil { + return nil, err + } + + users := make([]*User, 0, len(fileInfos)) + + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + continue + } + + name := fileInfo.Name() + + forwards, err := r.userForwards(domainName, name) + if err != nil { + return nil, err + } + + hashedPassword, ok := hashedPasswords[name] + if !ok { + hashedPassword = "" + } + + user, err := NewUser(name, hashedPassword, forwards) + if err != nil { + continue + } + + users = append(users, user) + } + + return users, nil +} + +// User returns a User of the input name. +func (r *Repository) User(domainName, userName string) (*User, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + if !validUserName(userName) { + return nil, ErrInvalidUserName + } + + hashedPasswords, err := r.usersHashedPassword(domainName) + if err != nil { + return nil, err + } + + fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName, userName)) + if err != nil { + return nil, err + } + + if !fileInfo.IsDir() { + return nil, nil + } + + name := userName + + forwards, err := r.userForwards(domainName, name) + if err != nil { + return nil, err + } + + hashedPassword, ok := hashedPasswords[name] + if !ok { + hashedPassword = "" + } + + user, err := NewUser(name, hashedPassword, forwards) + if err != nil { + return nil, err + } + + return user, nil +} + +// usersHashedPassword returns a string map of usernames to the hashed password. +func (r *Repository) usersHashedPassword(domainName string) (map[string]string, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword)) + if err != nil { + return nil, err + } + + hashedPasswords := map[string]string{} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + words := strings.Split(scanner.Text(), ":") + if len(words) != 2 { + return nil, ErrInvalidFormatUsersPassword + } + + name := words[0] + hashedPassword := words[1] + + hashedPasswords[name] = hashedPassword + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return hashedPasswords, nil +} + +// userForwards returns a string slice of forwards that the input name has. +func (r *Repository) userForwards(domainName, userName string) ([]string, error) { + user, err := r.User(domainName, userName) + if err != nil { + return nil, err + } + if user == nil { + return nil, ErrUserNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return []string{}, nil + } + + return nil, err + } + + forwards := make([]string, 0, 5) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + forwards = append(forwards, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return forwards, nil +} + +// AliasUsers returns a AliasUser slice. +func (r *Repository) AliasUsers(domainName string) ([]*AliasUser, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameAliasUsers)) + if err != nil { + return nil, err + } + + aliasUsers := make([]*AliasUser, 0, 50) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + words := strings.Split(scanner.Text(), ":") + if len(words) != 2 { + return nil, ErrInvalidFormatAliasUsers + } + + name := words[0] + targets := strings.Split(words[1], ",") + + aliasUser, err := NewAliasUser(name, targets) + if err != nil { + return nil, err + } + + aliasUsers = append(aliasUsers, aliasUser) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return aliasUsers, nil +} + +// AliasUser returns a AliasUser of the input name. +func (r *Repository) AliasUser(domainName, aliasUserName string) (*AliasUser, error) { + aliasUsers, err := r.AliasUsers(domainName) + if err != nil { + return nil, err + } + + for _, aliasUser := range aliasUsers { + if aliasUser.Name() == aliasUserName { + return aliasUser, nil + } + } + + return nil, nil +} + +// CatchAllUser returns a CatchAllUser that the input name has. +func (r *Repository) CatchAllUser(domainName string) (*CatchAllUser, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser)) + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(file) + scanner.Scan() + + name := scanner.Text() + + if err := scanner.Err(); err != nil { + return nil, err + } + + if name == "" { + return nil, nil + } + + catchAllUser, err := NewCatchAllUser(name) + if err != nil { + return nil, err + } + + return catchAllUser, nil +} diff --git a/repositoryconfig.go b/repositoryconfig.go new file mode 100644 index 0000000..32bf2af --- /dev/null +++ b/repositoryconfig.go @@ -0,0 +1,214 @@ +package mailfull + +import ( + "errors" + "os" + "path/filepath" + "syscall" + + "github.com/BurntSushi/toml" +) + +// Errors for the Repository. +var ( + ErrInvalidRepository = errors.New("invalid repository") + ErrNotRepository = errors.New("not a Mailfull repository (or any of the parent directories)") + ErrRepositoryExist = errors.New("a Mailfull repository exists") +) + +// RepositoryConfig is used to configure a Repository. +type RepositoryConfig struct { + DirDatabasePath string `toml:"dir_database"` + DirMailDataPath string `toml:"dir_maildata"` + Username string `toml:"username"` + Groupname string `toml:"groupname"` +} + +// DefaultRepositoryConfig returns a RepositoryConfig with default parameter. +func DefaultRepositoryConfig() *RepositoryConfig { + c := &RepositoryConfig{ + DirDatabasePath: "./etc", + DirMailDataPath: "./domains", + Username: "mailfull", + Groupname: "mailfull", + } + + return c +} + +// OpenRepository opens a Repository and creates a new Repository instance. +func OpenRepository(basePath string) (*Repository, error) { + rootPath, err := filepath.Abs(basePath) + if err != nil { + return nil, err + } + + for { + configDirPath := filepath.Join(rootPath, DirNameConfig) + + fi, errStat := os.Stat(configDirPath) + if errStat != nil { + if errStat.(*os.PathError).Err != syscall.ENOENT { + return nil, errStat + } + } else { + if fi.IsDir() { + break + } else { + return nil, ErrInvalidRepository + } + } + + parentPath := filepath.Clean(filepath.Join(rootPath, "..")) + if rootPath == parentPath { + return nil, ErrNotRepository + } + rootPath = parentPath + } + + configFilePath := filepath.Join(rootPath, DirNameConfig, FileNameConfig) + + fi, err := os.Stat(configFilePath) + if err != nil { + return nil, err + } + if fi.IsDir() { + return nil, ErrInvalidRepository + } + + configFile, err := os.Open(configFilePath) + if err != nil { + return nil, err + } + defer configFile.Close() + + c := DefaultRepositoryConfig() + if _, err = toml.DecodeReader(configFile, c); err != nil { + return nil, err + } + + if !filepath.IsAbs(c.DirDatabasePath) { + c.DirDatabasePath = filepath.Join(rootPath, c.DirDatabasePath) + } + if !filepath.IsAbs(c.DirMailDataPath) { + c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath) + } + + r, err := NewRepository(c) + if err != nil { + return nil, err + } + + return r, nil +} + +// InitRepository initializes the input directory as a Repository. +func InitRepository(rootPath string) error { + rootPath, err := filepath.Abs(rootPath) + if err != nil { + return err + } + + configDirPath := filepath.Join(rootPath, DirNameConfig) + + fi, err := os.Stat(configDirPath) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + if err = os.Mkdir(configDirPath, 0777); err != nil { + return err + } + } else { + return err + } + } else { + if !fi.IsDir() { + return ErrInvalidRepository + } + } + + configFilePath := filepath.Join(configDirPath, FileNameConfig) + + fi, err = os.Stat(configFilePath) + if err != nil { + if err.(*os.PathError).Err != syscall.ENOENT { + return err + } + } else { + if fi.IsDir() { + return ErrInvalidRepository + } + + return ErrRepositoryExist + } + + configFile, err := os.Create(configFilePath) + if err != nil { + return nil + } + defer configFile.Close() + + c := DefaultRepositoryConfig() + + enc := toml.NewEncoder(configFile) + if err := enc.Encode(c); err != nil { + return err + } + + if !filepath.IsAbs(c.DirDatabasePath) { + c.DirDatabasePath = filepath.Join(rootPath, c.DirDatabasePath) + } + if !filepath.IsAbs(c.DirMailDataPath) { + c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath) + } + + fi, err = os.Stat(c.DirDatabasePath) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + if err = os.Mkdir(c.DirDatabasePath, 0777); err != nil { + return err + } + } else { + return err + } + } else { + if !fi.IsDir() { + return ErrInvalidRepository + } + } + + fi, err = os.Stat(c.DirMailDataPath) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + if err = os.Mkdir(c.DirMailDataPath, 0777); err != nil { + return err + } + } else { + return err + } + } else { + if !fi.IsDir() { + return ErrInvalidRepository + } + } + + aliasDomainFileName := filepath.Join(c.DirMailDataPath, FileNameAliasDomains) + + fi, err = os.Stat(aliasDomainFileName) + if err != nil { + if err.(*os.PathError).Err != syscall.ENOENT { + return err + } + } else { + if fi.IsDir() { + return ErrInvalidRepository + } + } + + aliasDomainFile, err := os.Create(aliasDomainFileName) + if err != nil { + return nil + } + defer aliasDomainFile.Close() + + return nil +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..5899cc0 --- /dev/null +++ b/user.go @@ -0,0 +1,38 @@ +package mailfull + +// User represents a User. +type User struct { + name string + hashedPassword string + forwards []string +} + +// NewUser creates a new User instance. +func NewUser(name, hashedPassword string, forwards []string) (*User, error) { + if !validUserName(name) { + return nil, ErrInvalidUserName + } + + u := &User{ + name: name, + hashedPassword: hashedPassword, + forwards: forwards, + } + + return u, nil +} + +// Name returns name. +func (u *User) Name() string { + return u.name +} + +// HashedPassword returns hashedPassword. +func (u *User) HashedPassword() string { + return u.hashedPassword +} + +// Forwards returns forwards. +func (u *User) Forwards() []string { + return u.forwards +} diff --git a/valid.go b/valid.go new file mode 100644 index 0000000..f2e828a --- /dev/null +++ b/valid.go @@ -0,0 +1,52 @@ +package mailfull + +import ( + "errors" + "regexp" +) + +// Errors for incorrect format. +var ( + ErrInvalidDomainName = errors.New("Domain: name incorrect format") + ErrInvalidAliasDomainName = errors.New("AliasDomain: name incorrect format") + ErrInvalidAliasDomainTarget = errors.New("AliasDomain: target incorrect format") + ErrInvalidUserName = errors.New("User: name incorrect format") + ErrInvalidAliasUserName = errors.New("AliasUser: name incorrect format") + ErrInvalidAliasUserTarget = errors.New("AliasUser: target incorrect format") + ErrInvalidCatchAllUserName = errors.New("CatchAllUser: name incorrect format") +) + +// validDomainName returns true if the input is correct format. +func validDomainName(name string) bool { + return regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(name) +} + +// validAliasDomainName returns true if the input is correct format. +func validAliasDomainName(name string) bool { + return regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(name) +} + +// validAliasDomainTarget returns true if the input is correct format. +func validAliasDomainTarget(target string) bool { + return regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(target) +} + +// validUserName returns true if the input is correct format. +func validUserName(name string) bool { + return regexp.MustCompile(`^[^\s]+$`).MatchString(name) +} + +// validAliasUserName returns true if the input is correct format. +func validAliasUserName(name string) bool { + return regexp.MustCompile(`^[^\s]+$`).MatchString(name) +} + +// validAliasUserTarget returns true if the input is correct format. +func validAliasUserTarget(target string) bool { + return regexp.MustCompile(`^[^\s]+@([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(target) +} + +// validCatchAllUserName returns true if the input is correct format. +func validCatchAllUserName(name string) bool { + return regexp.MustCompile(`^[^\s]+$`).MatchString(name) +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..d2f4db1 --- /dev/null +++ b/version.go @@ -0,0 +1,4 @@ +package mailfull + +// Version is a version number. +const Version = "0.0.0" From 1074ab5b72dd761f2ccd310e352b8a85c826e375 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 10:09:32 +0900 Subject: [PATCH 02/39] Implement CLI scaffold --- .gitignore | 1 + cmd/mailfull/command/init.go | 41 +++++++++++++++++++++++++++ cmd/mailfull/command/main.go | 13 +++++++++ cmd/mailfull/main.go | 54 ++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/mailfull/command/init.go create mode 100644 cmd/mailfull/command/main.go create mode 100644 cmd/mailfull/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39a7681 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/cli/mailfull/mailfull diff --git a/cmd/mailfull/command/init.go b/cmd/mailfull/command/init.go new file mode 100644 index 0000000..11a818e --- /dev/null +++ b/cmd/mailfull/command/init.go @@ -0,0 +1,41 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// InitCommand represents a InitCommand. +type InitCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *InitCommand) Synopsis() string { + return "Initializes current directory as a Mailfull repository." +} + +// Help returns long-form help text. +func (c *InitCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s + +Description: + Initializes current directory as a Mailfull repository. +`, + c.CmdName, c.SubCmdName) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *InitCommand) Run(args []string) int { + if err := mailfull.InitRepository("."); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/main.go b/cmd/mailfull/command/main.go new file mode 100644 index 0000000..041cf7a --- /dev/null +++ b/cmd/mailfull/command/main.go @@ -0,0 +1,13 @@ +package command + +import ( + "github.com/mitchellh/cli" +) + +// Meta is for `*Command` struct. +type Meta struct { + UI *cli.BasicUi + CmdName string + SubCmdName string + Version string +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go new file mode 100644 index 0000000..7c7001f --- /dev/null +++ b/cmd/mailfull/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/directorz/mailfull-go" + "github.com/directorz/mailfull-go/cmd/mailfull/command" + "github.com/mitchellh/cli" +) + +var ( + version = mailfull.Version + gittag = "" +) + +func init() { + if gittag != "" { + version = version + "-" + gittag + } +} + +func main() { + c := &cli.CLI{ + Name: filepath.Base(os.Args[0]), + Version: version, + Args: os.Args[1:], + } + + meta := command.Meta{ + UI: &cli.BasicUi{ + Reader: os.Stdin, + Writer: os.Stdout, + ErrorWriter: os.Stderr, + }, + CmdName: c.Name, + Version: c.Version, + } + + c.Commands = map[string]cli.CommandFactory{ + "init": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.InitCommand{Meta: meta}, nil + }, + } + + exitCode, err := c.Run() + if err != nil { + fmt.Fprintf(meta.UI.ErrorWriter, "%v\n", err) + } + + os.Exit(exitCode) +} From f3e8f83d5ab833a832e4cf31cfd3192cb2e9def3 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 12:56:32 +0900 Subject: [PATCH 03/39] Fixed arguments check --- repository.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/repository.go b/repository.go index 0763019..e6ea7ad 100644 --- a/repository.go +++ b/repository.go @@ -273,12 +273,16 @@ func (r *Repository) usersHashedPassword(domainName string) (map[string]string, // userForwards returns a string slice of forwards that the input name has. func (r *Repository) userForwards(domainName, userName string) ([]string, error) { - user, err := r.User(domainName, userName) + domain, err := r.Domain(domainName) if err != nil { return nil, err } - if user == nil { - return nil, ErrUserNotExist + if domain == nil { + return nil, ErrDomainNotExist + } + + if !validUserName(userName) { + return nil, ErrInvalidUserName } file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) From c9e692a68d6eb44a4c4b66f1555d0b23372e51b8 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 13:41:07 +0900 Subject: [PATCH 04/39] Implement sort.Interface to required struct slice --- aliasdomain.go | 7 +++++++ aliasuser.go | 7 +++++++ domain.go | 7 +++++++ user.go | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/aliasdomain.go b/aliasdomain.go index 165ebf2..2a59b57 100644 --- a/aliasdomain.go +++ b/aliasdomain.go @@ -6,6 +6,13 @@ type AliasDomain struct { target string } +// AliasDomainSlice attaches the methods of sort.Interface to []*AliasDomain. +type AliasDomainSlice []*AliasDomain + +func (p AliasDomainSlice) Len() int { return len(p) } +func (p AliasDomainSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } +func (p AliasDomainSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + // NewAliasDomain creates a new AliasDomain instance. func NewAliasDomain(name, target string) (*AliasDomain, error) { if !validAliasDomainName(name) { diff --git a/aliasuser.go b/aliasuser.go index 7b04696..c8deea2 100644 --- a/aliasuser.go +++ b/aliasuser.go @@ -13,6 +13,13 @@ type AliasUser struct { targets []string } +// AliasUserSlice attaches the methods of sort.Interface to []*AliasUser. +type AliasUserSlice []*AliasUser + +func (p AliasUserSlice) Len() int { return len(p) } +func (p AliasUserSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } +func (p AliasUserSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + // NewAliasUser creates a new AliasUser instance. func NewAliasUser(name string, targets []string) (*AliasUser, error) { if !validAliasUserName(name) { diff --git a/domain.go b/domain.go index 1ca5d37..c1b8cf2 100644 --- a/domain.go +++ b/domain.go @@ -8,6 +8,13 @@ type Domain struct { CatchAllUser *CatchAllUser } +// DomainSlice attaches the methods of sort.Interface to []*Domain. +type DomainSlice []*Domain + +func (p DomainSlice) Len() int { return len(p) } +func (p DomainSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } +func (p DomainSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + // NewDomain creates a new Domain instance. func NewDomain(name string) (*Domain, error) { if !validDomainName(name) { diff --git a/user.go b/user.go index 5899cc0..b6e163b 100644 --- a/user.go +++ b/user.go @@ -7,6 +7,13 @@ type User struct { forwards []string } +// UserSlice attaches the methods of sort.Interface to []*User. +type UserSlice []*User + +func (p UserSlice) Len() int { return len(p) } +func (p UserSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } +func (p UserSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + // NewUser creates a new User instance. func NewUser(name, hashedPassword string, forwards []string) (*User, error) { if !validUserName(name) { From c7aec5b0ba51d904ab85e5a9e2db183bd43a309c Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 13:49:37 +0900 Subject: [PATCH 05/39] Fixed existence check. --- repository.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/repository.go b/repository.go index e6ea7ad..f069390 100644 --- a/repository.go +++ b/repository.go @@ -69,6 +69,10 @@ func (r *Repository) Domain(domainName string) (*Domain, error) { fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName)) if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return nil, nil + } + return nil, err } @@ -207,6 +211,10 @@ func (r *Repository) User(domainName, userName string) (*User, error) { fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName, userName)) if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return nil, nil + } + return nil, err } From 70b036b6c1526163ccf4587ab55ed6af3ccd2338 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 17:06:36 +0900 Subject: [PATCH 06/39] Implement `domainlist` subcommand --- cmd/mailfull/command/domainlist.go | 55 ++++++++++++++++++++++++++++++ cmd/mailfull/main.go | 4 +++ 2 files changed, 59 insertions(+) create mode 100644 cmd/mailfull/command/domainlist.go diff --git a/cmd/mailfull/command/domainlist.go b/cmd/mailfull/command/domainlist.go new file mode 100644 index 0000000..11657d2 --- /dev/null +++ b/cmd/mailfull/command/domainlist.go @@ -0,0 +1,55 @@ +package command + +import ( + "fmt" + "sort" + + "github.com/directorz/mailfull-go" +) + +// DomainListCommand represents a DomainListCommand. +type DomainListCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *DomainListCommand) Synopsis() string { + return "Show all domains." +} + +// Help returns long-form help text. +func (c *DomainListCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s + +Description: + Show all domains. +`, + c.CmdName, c.SubCmdName) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *DomainListCommand) Run(args []string) int { + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + domains, err := repo.Domains() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + sort.Sort(mailfull.DomainSlice(domains)) + + for _, domain := range domains { + fmt.Fprintf(c.UI.Writer, "%s\n", domain.Name()) + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 7c7001f..8bff108 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -43,6 +43,10 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.InitCommand{Meta: meta}, nil }, + "domainlist": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.DomainListCommand{Meta: meta}, nil + }, } exitCode, err := c.Run() From b867c701b8e71695f3e69aadeae70b2c5f756a96 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 17:13:59 +0900 Subject: [PATCH 07/39] Cosmetic changes --- repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repository.go b/repository.go index f069390..407f4be 100644 --- a/repository.go +++ b/repository.go @@ -296,7 +296,7 @@ func (r *Repository) userForwards(domainName, userName string) ([]string, error) file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) if err != nil { if err.(*os.PathError).Err == syscall.ENOENT { - return []string{}, nil + return nil, nil } return nil, err From dc39435103517097463872e0f349c810e1fcfe20 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 18:19:50 +0900 Subject: [PATCH 08/39] Moved functions --- aliasdomain.go | 57 ++++++ aliasuser.go | 66 ++++++- catchalluser.go | 42 +++++ domain.go | 63 +++++++ repository.go | 411 -------------------------------------------- repositoryconfig.go | 24 +++ user.go | 185 ++++++++++++++++++++ 7 files changed, 436 insertions(+), 412 deletions(-) delete mode 100644 repository.go diff --git a/aliasdomain.go b/aliasdomain.go index 2a59b57..0010aa7 100644 --- a/aliasdomain.go +++ b/aliasdomain.go @@ -1,5 +1,12 @@ package mailfull +import ( + "bufio" + "os" + "path/filepath" + "strings" +) + // AliasDomain represents a AliasDomain. type AliasDomain struct { name string @@ -39,3 +46,53 @@ func (ad *AliasDomain) Name() string { func (ad *AliasDomain) Target() string { return ad.target } + +// AliasDomains returns a AliasDomain slice. +func (r *Repository) AliasDomains() ([]*AliasDomain, error) { + file, err := os.Open(filepath.Join(r.DirMailDataPath, FileNameAliasDomains)) + if err != nil { + return nil, err + } + + aliasDomains := make([]*AliasDomain, 0, 10) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + words := strings.Split(scanner.Text(), ":") + if len(words) != 2 { + return nil, ErrInvalidFormatAliasDomain + } + + name := words[0] + target := words[1] + + aliasDomain, err := NewAliasDomain(name, target) + if err != nil { + return nil, err + } + + aliasDomains = append(aliasDomains, aliasDomain) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return aliasDomains, nil +} + +// AliasDomain returns a AliasDomain of the input name. +func (r *Repository) AliasDomain(aliasDomainName string) (*AliasDomain, error) { + aliasDomains, err := r.AliasDomains() + if err != nil { + return nil, err + } + + for _, aliasDomain := range aliasDomains { + if aliasDomain.Name() == aliasDomainName { + return aliasDomain, nil + } + } + + return nil, nil +} diff --git a/aliasuser.go b/aliasuser.go index c8deea2..fa3d556 100644 --- a/aliasuser.go +++ b/aliasuser.go @@ -1,6 +1,12 @@ package mailfull -import "errors" +import ( + "bufio" + "errors" + "os" + "path/filepath" + "strings" +) // Errors for parameter. var ( @@ -53,3 +59,61 @@ func (au *AliasUser) Name() string { func (au *AliasUser) Targets() []string { return au.targets } + +// AliasUsers returns a AliasUser slice. +func (r *Repository) AliasUsers(domainName string) ([]*AliasUser, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameAliasUsers)) + if err != nil { + return nil, err + } + + aliasUsers := make([]*AliasUser, 0, 50) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + words := strings.Split(scanner.Text(), ":") + if len(words) != 2 { + return nil, ErrInvalidFormatAliasUsers + } + + name := words[0] + targets := strings.Split(words[1], ",") + + aliasUser, err := NewAliasUser(name, targets) + if err != nil { + return nil, err + } + + aliasUsers = append(aliasUsers, aliasUser) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return aliasUsers, nil +} + +// AliasUser returns a AliasUser of the input name. +func (r *Repository) AliasUser(domainName, aliasUserName string) (*AliasUser, error) { + aliasUsers, err := r.AliasUsers(domainName) + if err != nil { + return nil, err + } + + for _, aliasUser := range aliasUsers { + if aliasUser.Name() == aliasUserName { + return aliasUser, nil + } + } + + return nil, nil +} diff --git a/catchalluser.go b/catchalluser.go index 61e93bf..d96a886 100644 --- a/catchalluser.go +++ b/catchalluser.go @@ -1,5 +1,11 @@ package mailfull +import ( + "bufio" + "os" + "path/filepath" +) + // CatchAllUser represents a CatchAllUser. type CatchAllUser struct { name string @@ -22,3 +28,39 @@ func NewCatchAllUser(name string) (*CatchAllUser, error) { func (cu *CatchAllUser) Name() string { return cu.name } + +// CatchAllUser returns a CatchAllUser that the input name has. +func (r *Repository) CatchAllUser(domainName string) (*CatchAllUser, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser)) + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(file) + scanner.Scan() + + name := scanner.Text() + + if err := scanner.Err(); err != nil { + return nil, err + } + + if name == "" { + return nil, nil + } + + catchAllUser, err := NewCatchAllUser(name) + if err != nil { + return nil, err + } + + return catchAllUser, nil +} diff --git a/domain.go b/domain.go index c1b8cf2..d881bc1 100644 --- a/domain.go +++ b/domain.go @@ -1,5 +1,12 @@ package mailfull +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" +) + // Domain represents a Domain. type Domain struct { name string @@ -32,3 +39,59 @@ func NewDomain(name string) (*Domain, error) { func (d *Domain) Name() string { return d.name } + +// Domains returns a Domain slice. +func (r *Repository) Domains() ([]*Domain, error) { + fileInfos, err := ioutil.ReadDir(r.DirMailDataPath) + if err != nil { + return nil, err + } + + domains := make([]*Domain, 0, len(fileInfos)) + + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + continue + } + + name := fileInfo.Name() + + domain, err := NewDomain(name) + if err != nil { + continue + } + + domains = append(domains, domain) + } + + return domains, nil +} + +// Domain returns a Domain of the input name. +func (r *Repository) Domain(domainName string) (*Domain, error) { + if !validDomainName(domainName) { + return nil, ErrInvalidDomainName + } + + fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName)) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return nil, nil + } + + return nil, err + } + + if !fileInfo.IsDir() { + return nil, nil + } + + name := domainName + + domain, err := NewDomain(name) + if err != nil { + return nil, err + } + + return domain, nil +} diff --git a/repository.go b/repository.go deleted file mode 100644 index 407f4be..0000000 --- a/repository.go +++ /dev/null @@ -1,411 +0,0 @@ -package mailfull - -import ( - "bufio" - "errors" - "io/ioutil" - "os" - "path/filepath" - "strings" - "syscall" -) - -// Errors for the operation of the Repository. -var ( - ErrDomainNotExist = errors.New("Domain: not exist") - ErrUserNotExist = errors.New("User: not exist") - - ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") - ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") - ErrInvalidFormatAliasUsers = errors.New("AliasUsers: file invalid format") -) - -// Repository represents a Repository. -type Repository struct { - *RepositoryConfig -} - -// NewRepository creates a new Repository instance. -func NewRepository(c *RepositoryConfig) (*Repository, error) { - r := &Repository{ - RepositoryConfig: c, - } - - return r, nil -} - -// Domains returns a Domain slice. -func (r *Repository) Domains() ([]*Domain, error) { - fileInfos, err := ioutil.ReadDir(r.DirMailDataPath) - if err != nil { - return nil, err - } - - domains := make([]*Domain, 0, len(fileInfos)) - - for _, fileInfo := range fileInfos { - if !fileInfo.IsDir() { - continue - } - - name := fileInfo.Name() - - domain, err := NewDomain(name) - if err != nil { - continue - } - - domains = append(domains, domain) - } - - return domains, nil -} - -// Domain returns a Domain of the input name. -func (r *Repository) Domain(domainName string) (*Domain, error) { - if !validDomainName(domainName) { - return nil, ErrInvalidDomainName - } - - fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName)) - if err != nil { - if err.(*os.PathError).Err == syscall.ENOENT { - return nil, nil - } - - return nil, err - } - - if !fileInfo.IsDir() { - return nil, nil - } - - name := domainName - - domain, err := NewDomain(name) - if err != nil { - return nil, err - } - - return domain, nil -} - -// AliasDomains returns a AliasDomain slice. -func (r *Repository) AliasDomains() ([]*AliasDomain, error) { - file, err := os.Open(filepath.Join(r.DirMailDataPath, FileNameAliasDomains)) - if err != nil { - return nil, err - } - - aliasDomains := make([]*AliasDomain, 0, 10) - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - words := strings.Split(scanner.Text(), ":") - if len(words) != 2 { - return nil, ErrInvalidFormatAliasDomain - } - - name := words[0] - target := words[1] - - aliasDomain, err := NewAliasDomain(name, target) - if err != nil { - return nil, err - } - - aliasDomains = append(aliasDomains, aliasDomain) - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return aliasDomains, nil -} - -// AliasDomain returns a AliasDomain of the input name. -func (r *Repository) AliasDomain(aliasDomainName string) (*AliasDomain, error) { - aliasDomains, err := r.AliasDomains() - if err != nil { - return nil, err - } - - for _, aliasDomain := range aliasDomains { - if aliasDomain.Name() == aliasDomainName { - return aliasDomain, nil - } - } - - return nil, nil -} - -// Users returns a User slice. -func (r *Repository) Users(domainName string) ([]*User, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist - } - - hashedPasswords, err := r.usersHashedPassword(domainName) - if err != nil { - return nil, err - } - - fileInfos, err := ioutil.ReadDir(filepath.Join(r.DirMailDataPath, domainName)) - if err != nil { - return nil, err - } - - users := make([]*User, 0, len(fileInfos)) - - for _, fileInfo := range fileInfos { - if !fileInfo.IsDir() { - continue - } - - name := fileInfo.Name() - - forwards, err := r.userForwards(domainName, name) - if err != nil { - return nil, err - } - - hashedPassword, ok := hashedPasswords[name] - if !ok { - hashedPassword = "" - } - - user, err := NewUser(name, hashedPassword, forwards) - if err != nil { - continue - } - - users = append(users, user) - } - - return users, nil -} - -// User returns a User of the input name. -func (r *Repository) User(domainName, userName string) (*User, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist - } - - if !validUserName(userName) { - return nil, ErrInvalidUserName - } - - hashedPasswords, err := r.usersHashedPassword(domainName) - if err != nil { - return nil, err - } - - fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName, userName)) - if err != nil { - if err.(*os.PathError).Err == syscall.ENOENT { - return nil, nil - } - - return nil, err - } - - if !fileInfo.IsDir() { - return nil, nil - } - - name := userName - - forwards, err := r.userForwards(domainName, name) - if err != nil { - return nil, err - } - - hashedPassword, ok := hashedPasswords[name] - if !ok { - hashedPassword = "" - } - - user, err := NewUser(name, hashedPassword, forwards) - if err != nil { - return nil, err - } - - return user, nil -} - -// usersHashedPassword returns a string map of usernames to the hashed password. -func (r *Repository) usersHashedPassword(domainName string) (map[string]string, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist - } - - file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword)) - if err != nil { - return nil, err - } - - hashedPasswords := map[string]string{} - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - words := strings.Split(scanner.Text(), ":") - if len(words) != 2 { - return nil, ErrInvalidFormatUsersPassword - } - - name := words[0] - hashedPassword := words[1] - - hashedPasswords[name] = hashedPassword - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return hashedPasswords, nil -} - -// userForwards returns a string slice of forwards that the input name has. -func (r *Repository) userForwards(domainName, userName string) ([]string, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist - } - - if !validUserName(userName) { - return nil, ErrInvalidUserName - } - - file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) - if err != nil { - if err.(*os.PathError).Err == syscall.ENOENT { - return nil, nil - } - - return nil, err - } - - forwards := make([]string, 0, 5) - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - forwards = append(forwards, scanner.Text()) - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return forwards, nil -} - -// AliasUsers returns a AliasUser slice. -func (r *Repository) AliasUsers(domainName string) ([]*AliasUser, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist - } - - file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameAliasUsers)) - if err != nil { - return nil, err - } - - aliasUsers := make([]*AliasUser, 0, 50) - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - words := strings.Split(scanner.Text(), ":") - if len(words) != 2 { - return nil, ErrInvalidFormatAliasUsers - } - - name := words[0] - targets := strings.Split(words[1], ",") - - aliasUser, err := NewAliasUser(name, targets) - if err != nil { - return nil, err - } - - aliasUsers = append(aliasUsers, aliasUser) - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return aliasUsers, nil -} - -// AliasUser returns a AliasUser of the input name. -func (r *Repository) AliasUser(domainName, aliasUserName string) (*AliasUser, error) { - aliasUsers, err := r.AliasUsers(domainName) - if err != nil { - return nil, err - } - - for _, aliasUser := range aliasUsers { - if aliasUser.Name() == aliasUserName { - return aliasUser, nil - } - } - - return nil, nil -} - -// CatchAllUser returns a CatchAllUser that the input name has. -func (r *Repository) CatchAllUser(domainName string) (*CatchAllUser, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist - } - - file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser)) - if err != nil { - return nil, err - } - - scanner := bufio.NewScanner(file) - scanner.Scan() - - name := scanner.Text() - - if err := scanner.Err(); err != nil { - return nil, err - } - - if name == "" { - return nil, nil - } - - catchAllUser, err := NewCatchAllUser(name) - if err != nil { - return nil, err - } - - return catchAllUser, nil -} diff --git a/repositoryconfig.go b/repositoryconfig.go index 32bf2af..25474e4 100644 --- a/repositoryconfig.go +++ b/repositoryconfig.go @@ -16,6 +16,16 @@ var ( ErrRepositoryExist = errors.New("a Mailfull repository exists") ) +// Errors for the operation of the Repository. +var ( + ErrDomainNotExist = errors.New("Domain: not exist") + ErrUserNotExist = errors.New("User: not exist") + + ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") + ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") + ErrInvalidFormatAliasUsers = errors.New("AliasUsers: file invalid format") +) + // RepositoryConfig is used to configure a Repository. type RepositoryConfig struct { DirDatabasePath string `toml:"dir_database"` @@ -36,6 +46,20 @@ func DefaultRepositoryConfig() *RepositoryConfig { return c } +// Repository represents a Repository. +type Repository struct { + *RepositoryConfig +} + +// NewRepository creates a new Repository instance. +func NewRepository(c *RepositoryConfig) (*Repository, error) { + r := &Repository{ + RepositoryConfig: c, + } + + return r, nil +} + // OpenRepository opens a Repository and creates a new Repository instance. func OpenRepository(basePath string) (*Repository, error) { rootPath, err := filepath.Abs(basePath) diff --git a/user.go b/user.go index b6e163b..6fa9f74 100644 --- a/user.go +++ b/user.go @@ -1,5 +1,14 @@ package mailfull +import ( + "bufio" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" +) + // User represents a User. type User struct { name string @@ -43,3 +52,179 @@ func (u *User) HashedPassword() string { func (u *User) Forwards() []string { return u.forwards } + +// Users returns a User slice. +func (r *Repository) Users(domainName string) ([]*User, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + hashedPasswords, err := r.usersHashedPassword(domainName) + if err != nil { + return nil, err + } + + fileInfos, err := ioutil.ReadDir(filepath.Join(r.DirMailDataPath, domainName)) + if err != nil { + return nil, err + } + + users := make([]*User, 0, len(fileInfos)) + + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + continue + } + + name := fileInfo.Name() + + forwards, err := r.userForwards(domainName, name) + if err != nil { + return nil, err + } + + hashedPassword, ok := hashedPasswords[name] + if !ok { + hashedPassword = "" + } + + user, err := NewUser(name, hashedPassword, forwards) + if err != nil { + continue + } + + users = append(users, user) + } + + return users, nil +} + +// User returns a User of the input name. +func (r *Repository) User(domainName, userName string) (*User, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + if !validUserName(userName) { + return nil, ErrInvalidUserName + } + + hashedPasswords, err := r.usersHashedPassword(domainName) + if err != nil { + return nil, err + } + + fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName, userName)) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return nil, nil + } + + return nil, err + } + + if !fileInfo.IsDir() { + return nil, nil + } + + name := userName + + forwards, err := r.userForwards(domainName, name) + if err != nil { + return nil, err + } + + hashedPassword, ok := hashedPasswords[name] + if !ok { + hashedPassword = "" + } + + user, err := NewUser(name, hashedPassword, forwards) + if err != nil { + return nil, err + } + + return user, nil +} + +// usersHashedPassword returns a string map of usernames to the hashed password. +func (r *Repository) usersHashedPassword(domainName string) (map[string]string, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword)) + if err != nil { + return nil, err + } + + hashedPasswords := map[string]string{} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + words := strings.Split(scanner.Text(), ":") + if len(words) != 2 { + return nil, ErrInvalidFormatUsersPassword + } + + name := words[0] + hashedPassword := words[1] + + hashedPasswords[name] = hashedPassword + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return hashedPasswords, nil +} + +// userForwards returns a string slice of forwards that the input name has. +func (r *Repository) userForwards(domainName, userName string) ([]string, error) { + domain, err := r.Domain(domainName) + if err != nil { + return nil, err + } + if domain == nil { + return nil, ErrDomainNotExist + } + + if !validUserName(userName) { + return nil, ErrInvalidUserName + } + + file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return nil, nil + } + + return nil, err + } + + forwards := make([]string, 0, 5) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + forwards = append(forwards, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return forwards, nil +} From c530f461a1dde13dd8a38167c6ad8841c14571ce Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 18:20:13 +0900 Subject: [PATCH 09/39] Renamed --- repositoryconfig.go => repository.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename repositoryconfig.go => repository.go (100%) diff --git a/repositoryconfig.go b/repository.go similarity index 100% rename from repositoryconfig.go rename to repository.go From 1d1011ebc461974a9a26513e05faa191aa3560f8 Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 1 Aug 2016 13:53:10 +0900 Subject: [PATCH 10/39] Change regexp pattern --- valid.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/valid.go b/valid.go index f2e828a..8061cd3 100644 --- a/valid.go +++ b/valid.go @@ -18,35 +18,35 @@ var ( // validDomainName returns true if the input is correct format. func validDomainName(name string) bool { - return regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(name) + return regexp.MustCompile(`^([A-Za-z0-9\-]+\.)*[A-Za-z]+$`).MatchString(name) } // validAliasDomainName returns true if the input is correct format. func validAliasDomainName(name string) bool { - return regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(name) + return regexp.MustCompile(`^([A-Za-z0-9\-]+\.)*[A-Za-z]+$`).MatchString(name) } // validAliasDomainTarget returns true if the input is correct format. func validAliasDomainTarget(target string) bool { - return regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(target) + return regexp.MustCompile(`^([A-Za-z0-9\-]+\.)*[A-Za-z]+$`).MatchString(target) } // validUserName returns true if the input is correct format. func validUserName(name string) bool { - return regexp.MustCompile(`^[^\s]+$`).MatchString(name) + return regexp.MustCompile(`^[^\s@]+$`).MatchString(name) } // validAliasUserName returns true if the input is correct format. func validAliasUserName(name string) bool { - return regexp.MustCompile(`^[^\s]+$`).MatchString(name) + return regexp.MustCompile(`^[^\s@]+$`).MatchString(name) } // validAliasUserTarget returns true if the input is correct format. func validAliasUserTarget(target string) bool { - return regexp.MustCompile(`^[^\s]+@([A-Za-z0-9][A-Za-z0-9\-]{1,61}[A-Za-z0-9]\.)*[A-Za-z]+$`).MatchString(target) + return regexp.MustCompile(`^[^\s@]+@([A-Za-z0-9\-]+\.)*[A-Za-z]+$`).MatchString(target) } // validCatchAllUserName returns true if the input is correct format. func validCatchAllUserName(name string) bool { - return regexp.MustCompile(`^[^\s]+$`).MatchString(name) + return regexp.MustCompile(`^[^\s@]+$`).MatchString(name) } From b9bc3707f34153dac8ea988ed6dd21b883669815 Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 1 Aug 2016 16:55:59 +0900 Subject: [PATCH 11/39] Add Normalize method for RepositoryConfig --- repository.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/repository.go b/repository.go index 25474e4..9a1ae7c 100644 --- a/repository.go +++ b/repository.go @@ -34,6 +34,16 @@ type RepositoryConfig struct { Groupname string `toml:"groupname"` } +// Normalize normalizes paramaters of the RepositoryConfig. +func (c *RepositoryConfig) Normalize(rootPath string) { + if !filepath.IsAbs(c.DirDatabasePath) { + c.DirDatabasePath = filepath.Join(rootPath, c.DirDatabasePath) + } + if !filepath.IsAbs(c.DirMailDataPath) { + c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath) + } +} + // DefaultRepositoryConfig returns a RepositoryConfig with default parameter. func DefaultRepositoryConfig() *RepositoryConfig { c := &RepositoryConfig{ @@ -111,12 +121,7 @@ func OpenRepository(basePath string) (*Repository, error) { return nil, err } - if !filepath.IsAbs(c.DirDatabasePath) { - c.DirDatabasePath = filepath.Join(rootPath, c.DirDatabasePath) - } - if !filepath.IsAbs(c.DirMailDataPath) { - c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath) - } + c.Normalize(rootPath) r, err := NewRepository(c) if err != nil { @@ -178,12 +183,7 @@ func InitRepository(rootPath string) error { return err } - if !filepath.IsAbs(c.DirDatabasePath) { - c.DirDatabasePath = filepath.Join(rootPath, c.DirDatabasePath) - } - if !filepath.IsAbs(c.DirMailDataPath) { - c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath) - } + c.Normalize(rootPath) fi, err = os.Stat(c.DirDatabasePath) if err != nil { From cd6f8097e75e8d69b306ca7ae33603754d10fe6c Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 1 Aug 2016 16:59:55 +0900 Subject: [PATCH 12/39] Add fields to RepositoryConfig --- repository.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/repository.go b/repository.go index 9a1ae7c..e9abecc 100644 --- a/repository.go +++ b/repository.go @@ -32,6 +32,8 @@ type RepositoryConfig struct { DirMailDataPath string `toml:"dir_maildata"` Username string `toml:"username"` Groupname string `toml:"groupname"` + CmdPostalias string `toml:"postalias"` + CmdPostmap string `toml:"postmap"` } // Normalize normalizes paramaters of the RepositoryConfig. @@ -42,6 +44,18 @@ func (c *RepositoryConfig) Normalize(rootPath string) { if !filepath.IsAbs(c.DirMailDataPath) { c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath) } + + if filepath.Base(c.CmdPostalias) != c.CmdPostalias { + if !filepath.IsAbs(c.CmdPostalias) { + c.CmdPostalias = filepath.Join(rootPath, c.CmdPostalias) + } + } + + if filepath.Base(c.CmdPostmap) != c.CmdPostmap { + if !filepath.IsAbs(c.CmdPostmap) { + c.CmdPostmap = filepath.Join(rootPath, c.CmdPostmap) + } + } } // DefaultRepositoryConfig returns a RepositoryConfig with default parameter. @@ -51,6 +65,8 @@ func DefaultRepositoryConfig() *RepositoryConfig { DirMailDataPath: "./domains", Username: "mailfull", Groupname: "mailfull", + CmdPostalias: "postalias", + CmdPostmap: "postmap", } return c From 7ae2d1422b8f5a9619815fdde041e744f239968a Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 1 Aug 2016 17:04:32 +0900 Subject: [PATCH 13/39] Implement parts for generating databases --- const.go | 7 ++ database.go | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++ maildata.go | 47 ++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 database.go create mode 100644 maildata.go diff --git a/const.go b/const.go index 98164a8..07beaa3 100644 --- a/const.go +++ b/const.go @@ -10,4 +10,11 @@ const ( FileNameUserForwards = ".forward" FileNameAliasUsers = ".valiases" FileNameCatchAllUser = ".vcatchall" + + FileNameDbDomains = "domains" + FileNameDbDestinations = "destinations" + FileNameDbMaildirs = "maildirs" + FileNameDbLocaltable = "localtable" + FileNameDbForwards = "forwards" + FileNameDbPasswords = "vpasswd" ) diff --git a/database.go b/database.go new file mode 100644 index 0000000..f9b684c --- /dev/null +++ b/database.go @@ -0,0 +1,201 @@ +package mailfull + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" +) + +// GenerateDatabases generates databases from the MailData directory. +func (r *Repository) GenerateDatabases(md *MailData) error { + sort.Sort(DomainSlice(md.Domains)) + sort.Sort(AliasDomainSlice(md.AliasDomains)) + + for _, domain := range md.Domains { + sort.Sort(UserSlice(domain.Users)) + sort.Sort(AliasUserSlice(domain.AliasUsers)) + } + + // Generate files + if err := r.generateDbDomains(md); err != nil { + return err + } + if err := r.generateDbDestinations(md); err != nil { + return err + } + if err := r.generateDbMaildirs(md); err != nil { + return err + } + if err := r.generateDbLocaltable(md); err != nil { + return err + } + if err := r.generateDbForwards(md); err != nil { + return err + } + if err := r.generateDbPasswords(md); err != nil { + return err + } + + // Generate DBs + if err := exec.Command(r.CmdPostmap, filepath.Join(r.DirDatabasePath, FileNameDbDomains)).Run(); err != nil { + return err + } + if err := exec.Command(r.CmdPostmap, filepath.Join(r.DirDatabasePath, FileNameDbDestinations)).Run(); err != nil { + return err + } + if err := exec.Command(r.CmdPostmap, filepath.Join(r.DirDatabasePath, FileNameDbMaildirs)).Run(); err != nil { + return err + } + if err := exec.Command(r.CmdPostmap, filepath.Join(r.DirDatabasePath, FileNameDbLocaltable)).Run(); err != nil { + return err + } + if err := exec.Command(r.CmdPostalias, filepath.Join(r.DirDatabasePath, FileNameDbForwards)).Run(); err != nil { + return err + } + + return nil +} + +func (r *Repository) generateDbDomains(md *MailData) error { + dbDomains, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbDomains)) + if err != nil { + return err + } + defer dbDomains.Close() + + for _, domain := range md.Domains { + fmt.Fprintf(dbDomains, "%s virtual\n", domain.Name()) + } + + for _, aliasDomain := range md.AliasDomains { + fmt.Fprintf(dbDomains, "%s virtual\n", aliasDomain.Name()) + } + + return nil +} + +func (r *Repository) generateDbDestinations(md *MailData) error { + dbDestinations, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbDestinations)) + if err != nil { + return err + } + defer dbDestinations.Close() + + for _, domain := range md.Domains { + // ho-ge.example.com -> ho_ge.example.com + underscoredDomainName := domain.Name() + underscoredDomainName = strings.Replace(underscoredDomainName, `-`, `_`, -1) + + for _, user := range domain.Users { + userName := user.Name() + if cu := domain.CatchAllUser; cu != nil && cu.Name() == user.Name() { + userName = "" + } + + if len(user.Forwards()) > 0 { + fmt.Fprintf(dbDestinations, "%s@%s %s|%s\n", userName, domain.Name(), underscoredDomainName, user.Name()) + } else { + fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", userName, domain.Name(), user.Name(), domain.Name()) + } + + for _, aliasDomain := range md.AliasDomains { + if aliasDomain.Target() == domain.Name() { + fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", userName, aliasDomain.Name(), user.Name(), domain.Name()) + } + } + } + + for _, aliasUser := range domain.AliasUsers { + fmt.Fprintf(dbDestinations, "%s@%s %s\n", aliasUser.Name(), domain.Name(), strings.Join(aliasUser.Targets(), ",")) + + for _, aliasDomain := range md.AliasDomains { + if aliasDomain.Target() == domain.Name() { + fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", aliasUser.Name(), aliasDomain.Name(), aliasUser.Name(), domain.Name()) + } + } + } + } + + return nil +} + +func (r *Repository) generateDbMaildirs(md *MailData) error { + dbMaildirs, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbMaildirs)) + if err != nil { + return err + } + defer dbMaildirs.Close() + + for _, domain := range md.Domains { + for _, user := range domain.Users { + fmt.Fprintf(dbMaildirs, "%s@%s %s/%s/Maildir/\n", user.Name(), domain.Name(), domain.Name(), user.Name()) + } + } + + return nil +} + +func (r *Repository) generateDbLocaltable(md *MailData) error { + dbLocaltable, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbLocaltable)) + if err != nil { + return err + } + defer dbLocaltable.Close() + + for _, domain := range md.Domains { + // ho-ge.example.com -> ho_ge\.example\.com + escapedDomainName := domain.Name() + escapedDomainName = strings.Replace(escapedDomainName, `-`, `_`, -1) + escapedDomainName = strings.Replace(escapedDomainName, `.`, `\.`, -1) + + fmt.Fprintf(dbLocaltable, "/^%s\\|.*$/ local\n", escapedDomainName) + } + + return nil +} + +func (r *Repository) generateDbForwards(md *MailData) error { + dbForwards, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbForwards)) + if err != nil { + return err + } + defer dbForwards.Close() + + for _, domain := range md.Domains { + // ho-ge.example.com -> ho_ge.example.com + underscoredDomainName := domain.Name() + underscoredDomainName = strings.Replace(underscoredDomainName, `-`, `_`, -1) + + for _, user := range domain.Users { + if len(user.Forwards()) > 0 { + fmt.Fprintf(dbForwards, "%s|%s:%s\n", underscoredDomainName, user.Name(), strings.Join(user.Forwards(), ",")) + } else { + fmt.Fprintf(dbForwards, "%s|%s:/dev/null\n", underscoredDomainName, user.Name()) + } + } + } + + // drop real user + fmt.Fprintf(dbForwards, "%s:/dev/null\n", r.Username) + + return nil +} + +func (r *Repository) generateDbPasswords(md *MailData) error { + dbPasswords, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbPasswords)) + if err != nil { + return err + } + defer dbPasswords.Close() + + for _, domain := range md.Domains { + for _, user := range domain.Users { + fmt.Fprintf(dbPasswords, "%s@%s:%s\n", user.Name(), domain.Name(), user.HashedPassword()) + } + } + + return nil +} diff --git a/maildata.go b/maildata.go new file mode 100644 index 0000000..7e32b38 --- /dev/null +++ b/maildata.go @@ -0,0 +1,47 @@ +package mailfull + +// MailData represents a MailData. +type MailData struct { + Domains []*Domain + AliasDomains []*AliasDomain +} + +// MailData returns a MailData. +func (r *Repository) MailData() (*MailData, error) { + domains, err := r.Domains() + if err != nil { + return nil, err + } + + aliasDomains, err := r.AliasDomains() + if err != nil { + return nil, err + } + + for _, domain := range domains { + users, err := r.Users(domain.Name()) + if err != nil { + return nil, err + } + domain.Users = users + + aliasUsers, err := r.AliasUsers(domain.Name()) + if err != nil { + return nil, err + } + domain.AliasUsers = aliasUsers + + catchAllUser, err := r.CatchAllUser(domain.Name()) + if err != nil { + return nil, err + } + domain.CatchAllUser = catchAllUser + } + + mailData := &MailData{ + Domains: domains, + AliasDomains: aliasDomains, + } + + return mailData, nil +} From dbc25edeb44c76a6f0c85cd018f313978a6043ac Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 1 Aug 2016 17:08:49 +0900 Subject: [PATCH 14/39] Implement `commit` subcommand --- cmd/mailfull/command/commit.go | 54 ++++++++++++++++++++++++++++++++++ cmd/mailfull/main.go | 4 +++ 2 files changed, 58 insertions(+) create mode 100644 cmd/mailfull/command/commit.go diff --git a/cmd/mailfull/command/commit.go b/cmd/mailfull/command/commit.go new file mode 100644 index 0000000..2bd15d7 --- /dev/null +++ b/cmd/mailfull/command/commit.go @@ -0,0 +1,54 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// CommitCommand represents a CommitCommand. +type CommitCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *CommitCommand) Synopsis() string { + return "Create a database from the structure of the MailData directory." +} + +// Help returns long-form help text. +func (c *CommitCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s + +Description: + Create a database from the structure of the MailData directory. +`, + c.CmdName, c.SubCmdName) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *CommitCommand) Run(args []string) int { + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 8bff108..9c48eee 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -47,6 +47,10 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.DomainListCommand{Meta: meta}, nil }, + "commit": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.CommitCommand{Meta: meta}, nil + }, } exitCode, err := c.Run() From 9202f61aa29aaa0425a7c1861609ede4b5345a7b Mon Sep 17 00:00:00 2001 From: teru Date: Tue, 2 Aug 2016 10:19:18 +0900 Subject: [PATCH 15/39] Add missing defer statements #2 --- aliasdomain.go | 1 + aliasuser.go | 1 + catchalluser.go | 1 + user.go | 2 ++ 4 files changed, 5 insertions(+) diff --git a/aliasdomain.go b/aliasdomain.go index 0010aa7..09a0c81 100644 --- a/aliasdomain.go +++ b/aliasdomain.go @@ -53,6 +53,7 @@ func (r *Repository) AliasDomains() ([]*AliasDomain, error) { if err != nil { return nil, err } + defer file.Close() aliasDomains := make([]*AliasDomain, 0, 10) diff --git a/aliasuser.go b/aliasuser.go index fa3d556..2adc98d 100644 --- a/aliasuser.go +++ b/aliasuser.go @@ -74,6 +74,7 @@ func (r *Repository) AliasUsers(domainName string) ([]*AliasUser, error) { if err != nil { return nil, err } + defer file.Close() aliasUsers := make([]*AliasUser, 0, 50) diff --git a/catchalluser.go b/catchalluser.go index d96a886..8ca2aba 100644 --- a/catchalluser.go +++ b/catchalluser.go @@ -43,6 +43,7 @@ func (r *Repository) CatchAllUser(domainName string) (*CatchAllUser, error) { if err != nil { return nil, err } + defer file.Close() scanner := bufio.NewScanner(file) scanner.Scan() diff --git a/user.go b/user.go index 6fa9f74..6a579e0 100644 --- a/user.go +++ b/user.go @@ -169,6 +169,7 @@ func (r *Repository) usersHashedPassword(domainName string) (map[string]string, if err != nil { return nil, err } + defer file.Close() hashedPasswords := map[string]string{} @@ -214,6 +215,7 @@ func (r *Repository) userForwards(domainName, userName string) ([]string, error) return nil, err } + defer file.Close() forwards := make([]string, 0, 5) From c7d3c622812808012ea23d1162034133f4c80198 Mon Sep 17 00:00:00 2001 From: teru Date: Tue, 2 Aug 2016 10:48:51 +0900 Subject: [PATCH 16/39] Add error handlings of `fmt.Fprintf` #1 --- database.go | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/database.go b/database.go index f9b684c..71c02c5 100644 --- a/database.go +++ b/database.go @@ -67,11 +67,15 @@ func (r *Repository) generateDbDomains(md *MailData) error { defer dbDomains.Close() for _, domain := range md.Domains { - fmt.Fprintf(dbDomains, "%s virtual\n", domain.Name()) + if _, err := fmt.Fprintf(dbDomains, "%s virtual\n", domain.Name()); err != nil { + return err + } } for _, aliasDomain := range md.AliasDomains { - fmt.Fprintf(dbDomains, "%s virtual\n", aliasDomain.Name()) + if _, err := fmt.Fprintf(dbDomains, "%s virtual\n", aliasDomain.Name()); err != nil { + return err + } } return nil @@ -96,24 +100,34 @@ func (r *Repository) generateDbDestinations(md *MailData) error { } if len(user.Forwards()) > 0 { - fmt.Fprintf(dbDestinations, "%s@%s %s|%s\n", userName, domain.Name(), underscoredDomainName, user.Name()) + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s|%s\n", userName, domain.Name(), underscoredDomainName, user.Name()); err != nil { + return err + } } else { - fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", userName, domain.Name(), user.Name(), domain.Name()) + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", userName, domain.Name(), user.Name(), domain.Name()); err != nil { + return err + } } for _, aliasDomain := range md.AliasDomains { if aliasDomain.Target() == domain.Name() { - fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", userName, aliasDomain.Name(), user.Name(), domain.Name()) + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", userName, aliasDomain.Name(), user.Name(), domain.Name()); err != nil { + return err + } } } } for _, aliasUser := range domain.AliasUsers { - fmt.Fprintf(dbDestinations, "%s@%s %s\n", aliasUser.Name(), domain.Name(), strings.Join(aliasUser.Targets(), ",")) + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s\n", aliasUser.Name(), domain.Name(), strings.Join(aliasUser.Targets(), ",")); err != nil { + return err + } for _, aliasDomain := range md.AliasDomains { if aliasDomain.Target() == domain.Name() { - fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", aliasUser.Name(), aliasDomain.Name(), aliasUser.Name(), domain.Name()) + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", aliasUser.Name(), aliasDomain.Name(), aliasUser.Name(), domain.Name()); err != nil { + return err + } } } } @@ -131,7 +145,9 @@ func (r *Repository) generateDbMaildirs(md *MailData) error { for _, domain := range md.Domains { for _, user := range domain.Users { - fmt.Fprintf(dbMaildirs, "%s@%s %s/%s/Maildir/\n", user.Name(), domain.Name(), domain.Name(), user.Name()) + if _, err := fmt.Fprintf(dbMaildirs, "%s@%s %s/%s/Maildir/\n", user.Name(), domain.Name(), domain.Name(), user.Name()); err != nil { + return err + } } } @@ -151,7 +167,9 @@ func (r *Repository) generateDbLocaltable(md *MailData) error { escapedDomainName = strings.Replace(escapedDomainName, `-`, `_`, -1) escapedDomainName = strings.Replace(escapedDomainName, `.`, `\.`, -1) - fmt.Fprintf(dbLocaltable, "/^%s\\|.*$/ local\n", escapedDomainName) + if _, err := fmt.Fprintf(dbLocaltable, "/^%s\\|.*$/ local\n", escapedDomainName); err != nil { + return err + } } return nil @@ -171,15 +189,21 @@ func (r *Repository) generateDbForwards(md *MailData) error { for _, user := range domain.Users { if len(user.Forwards()) > 0 { - fmt.Fprintf(dbForwards, "%s|%s:%s\n", underscoredDomainName, user.Name(), strings.Join(user.Forwards(), ",")) + if _, err := fmt.Fprintf(dbForwards, "%s|%s:%s\n", underscoredDomainName, user.Name(), strings.Join(user.Forwards(), ",")); err != nil { + return err + } } else { - fmt.Fprintf(dbForwards, "%s|%s:/dev/null\n", underscoredDomainName, user.Name()) + if _, err := fmt.Fprintf(dbForwards, "%s|%s:/dev/null\n", underscoredDomainName, user.Name()); err != nil { + return err + } } } } // drop real user - fmt.Fprintf(dbForwards, "%s:/dev/null\n", r.Username) + if _, err := fmt.Fprintf(dbForwards, "%s:/dev/null\n", r.Username); err != nil { + return err + } return nil } @@ -193,7 +217,9 @@ func (r *Repository) generateDbPasswords(md *MailData) error { for _, domain := range md.Domains { for _, user := range domain.Users { - fmt.Fprintf(dbPasswords, "%s@%s:%s\n", user.Name(), domain.Name(), user.HashedPassword()) + if _, err := fmt.Fprintf(dbPasswords, "%s@%s:%s\n", user.Name(), domain.Name(), user.HashedPassword()); err != nil { + return err + } } } From 215df0c8198f2a6438f9b4654a8e3dce30defb5f Mon Sep 17 00:00:00 2001 From: teru Date: Fri, 5 Aug 2016 18:26:54 +0900 Subject: [PATCH 17/39] implement setters and change to call it from constructor --- aliasdomain.go | 36 ++++++++++++++++++++++++++++-------- aliasuser.go | 49 ++++++++++++++++++++++++++++++++++--------------- user.go | 34 +++++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/aliasdomain.go b/aliasdomain.go index 09a0c81..5a01eaf 100644 --- a/aliasdomain.go +++ b/aliasdomain.go @@ -22,26 +22,46 @@ func (p AliasDomainSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // NewAliasDomain creates a new AliasDomain instance. func NewAliasDomain(name, target string) (*AliasDomain, error) { - if !validAliasDomainName(name) { - return nil, ErrInvalidAliasDomainName - } - if !validAliasDomainTarget(target) { - return nil, ErrInvalidAliasDomainTarget + ad := &AliasDomain{} + + if err := ad.setName(name); err != nil { + return nil, err } - ad := &AliasDomain{ - name: name, - target: target, + if err := ad.SetTarget(target); err != nil { + return nil, err } return ad, nil } +// setName sets the name. +func (ad *AliasDomain) setName(name string) error { + if !validAliasDomainName(name) { + return ErrInvalidAliasDomainName + } + + ad.name = name + + return nil +} + // Name returns name. func (ad *AliasDomain) Name() string { return ad.name } +// SetTarget sets the target. +func (ad *AliasDomain) SetTarget(target string) error { + if !validAliasDomainTarget(target) { + return ErrInvalidAliasDomainTarget + } + + ad.target = target + + return nil +} + // Target returns target. func (ad *AliasDomain) Target() string { return ad.target diff --git a/aliasuser.go b/aliasuser.go index 2adc98d..c5d4966 100644 --- a/aliasuser.go +++ b/aliasuser.go @@ -28,33 +28,52 @@ func (p AliasUserSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // NewAliasUser creates a new AliasUser instance. func NewAliasUser(name string, targets []string) (*AliasUser, error) { - if !validAliasUserName(name) { - return nil, ErrInvalidAliasUserName + au := &AliasUser{} + + if err := au.setName(name); err != nil { + return nil, err } - if len(targets) < 1 { - return nil, ErrNotEnoughAliasUserTargets - } - - for _, target := range targets { - if !validAliasUserTarget(target) { - return nil, ErrInvalidAliasUserTarget - } - } - - au := &AliasUser{ - name: name, - targets: targets, + if err := au.SetTargets(targets); err != nil { + return nil, err } return au, nil } +// setName sets the name. +func (au *AliasUser) setName(name string) error { + if !validAliasUserName(name) { + return ErrInvalidAliasUserName + } + + au.name = name + + return nil +} + // Name returns name. func (au *AliasUser) Name() string { return au.name } +// SetTargets sets targets. +func (au *AliasUser) SetTargets(targets []string) error { + if len(targets) < 1 { + return ErrNotEnoughAliasUserTargets + } + + for _, target := range targets { + if !validAliasUserTarget(target) { + return ErrInvalidAliasUserTarget + } + } + + au.targets = targets + + return nil +} + // Targets returns targets. func (au *AliasUser) Targets() []string { return au.targets diff --git a/user.go b/user.go index 6a579e0..b05f4f1 100644 --- a/user.go +++ b/user.go @@ -25,29 +25,49 @@ func (p UserSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // NewUser creates a new User instance. func NewUser(name, hashedPassword string, forwards []string) (*User, error) { - if !validUserName(name) { - return nil, ErrInvalidUserName + u := &User{} + + if err := u.setName(name); err != nil { + return nil, err } - u := &User{ - name: name, - hashedPassword: hashedPassword, - forwards: forwards, - } + u.SetHashedPassword(hashedPassword) + u.SetForwards(forwards) return u, nil } +// setName sets the name. +func (u *User) setName(name string) error { + if !validUserName(name) { + return ErrInvalidUserName + } + + u.name = name + + return nil +} + // Name returns name. func (u *User) Name() string { return u.name } +// SetHashedPassword sets the hashed password. +func (u *User) SetHashedPassword(hashedPassword string) { + u.hashedPassword = hashedPassword +} + // HashedPassword returns hashedPassword. func (u *User) HashedPassword() string { return u.hashedPassword } +// SetForwards sets forwards. +func (u *User) SetForwards(forwards []string) { + u.forwards = forwards +} + // Forwards returns forwards. func (u *User) Forwards() []string { return u.forwards From 8b274c8091d4264e63776f03c7b9869206d87875 Mon Sep 17 00:00:00 2001 From: teru Date: Fri, 5 Aug 2016 23:33:52 +0900 Subject: [PATCH 18/39] Implement create/remove a Domain --- domain.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ repository.go | 7 +++-- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/domain.go b/domain.go index d881bc1..1aa23e2 100644 --- a/domain.go +++ b/domain.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "syscall" + "time" ) // Domain represents a Domain. @@ -95,3 +96,77 @@ func (r *Repository) Domain(domainName string) (*Domain, error) { return domain, nil } + +// DomainCreate creates the input Domain. +func (r *Repository) DomainCreate(domain *Domain) error { + existDomain, err := r.Domain(domain.Name()) + if err != nil { + return err + } + if existDomain != nil { + return ErrDomainAlreadyExist + } + existAliasDomain, err := r.AliasDomain(domain.Name()) + if err != nil { + return err + } + if existAliasDomain != nil { + return ErrAliasDomainAlreadyExist + } + + domainDirPath := filepath.Join(r.DirMailDataPath, domain.Name()) + + if err := os.Mkdir(domainDirPath, 0777); err != nil { + return err + } + + usersPasswordFile, err := os.Create(filepath.Join(domainDirPath, FileNameUsersPassword)) + if err != nil { + return err + } + usersPasswordFile.Close() + + aliasUsersFile, err := os.Create(filepath.Join(domainDirPath, FileNameAliasUsers)) + if err != nil { + return err + } + aliasUsersFile.Close() + + catchAllUserFile, err := os.Create(filepath.Join(domainDirPath, FileNameCatchAllUser)) + if err != nil { + return err + } + catchAllUserFile.Close() + + return nil +} + +// DomainRemove removes a Domain of the input name. +func (r *Repository) DomainRemove(domainName string) error { + existDomain, err := r.Domain(domainName) + if err != nil { + return err + } + if existDomain == nil { + return ErrDomainNotExist + } + + aliasDomains, err := r.AliasDomains() + if err != nil { + return err + } + for _, aliasDomain := range aliasDomains { + if aliasDomain.Target() == domainName { + return ErrDomainIsAliasDomainTarget + } + } + + domainDirPath := filepath.Join(r.DirMailDataPath, domainName) + domainBackupDirPath := filepath.Join(r.DirMailDataPath, "."+domainName+".deleted."+time.Now().Format("20060102150405")) + + if err := os.Rename(domainDirPath, domainBackupDirPath); err != nil { + return err + } + + return nil +} diff --git a/repository.go b/repository.go index e9abecc..72e3318 100644 --- a/repository.go +++ b/repository.go @@ -18,8 +18,11 @@ var ( // Errors for the operation of the Repository. var ( - ErrDomainNotExist = errors.New("Domain: not exist") - ErrUserNotExist = errors.New("User: not exist") + ErrDomainNotExist = errors.New("Domain: not exist") + ErrDomainAlreadyExist = errors.New("Domain: already exist") + ErrDomainIsAliasDomainTarget = errors.New("Domain: is set as alias") + ErrAliasDomainAlreadyExist = errors.New("AliasDomain: already exist") + ErrUserNotExist = errors.New("User: not exist") ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") From 7c74b7773dec58c5eee9f54a3209786ab18f3680 Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 8 Aug 2016 10:28:39 +0900 Subject: [PATCH 19/39] cosmetic changes --- repository.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repository.go b/repository.go index 72e3318..7ddcb52 100644 --- a/repository.go +++ b/repository.go @@ -234,9 +234,9 @@ func InitRepository(rootPath string) error { } } - aliasDomainFileName := filepath.Join(c.DirMailDataPath, FileNameAliasDomains) + aliasDomainsFileName := filepath.Join(c.DirMailDataPath, FileNameAliasDomains) - fi, err = os.Stat(aliasDomainFileName) + fi, err = os.Stat(aliasDomainsFileName) if err != nil { if err.(*os.PathError).Err != syscall.ENOENT { return err @@ -247,11 +247,11 @@ func InitRepository(rootPath string) error { } } - aliasDomainFile, err := os.Create(aliasDomainFileName) + aliasDomainsFile, err := os.Create(aliasDomainsFileName) if err != nil { return nil } - defer aliasDomainFile.Close() + defer aliasDomainsFile.Close() return nil } From 47d4b7bd49aca4576fb11bcfa8a5134e2fc5bc8c Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 8 Aug 2016 10:30:00 +0900 Subject: [PATCH 20/39] Implement create/remove a AliasDomain --- aliasdomain.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ repository.go | 7 +++-- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/aliasdomain.go b/aliasdomain.go index 5a01eaf..9728caa 100644 --- a/aliasdomain.go +++ b/aliasdomain.go @@ -2,8 +2,10 @@ package mailfull import ( "bufio" + "fmt" "os" "path/filepath" + "sort" "strings" ) @@ -117,3 +119,84 @@ func (r *Repository) AliasDomain(aliasDomainName string) (*AliasDomain, error) { return nil, nil } + +// AliasDomainCreate creates the input AliasDomain. +func (r *Repository) AliasDomainCreate(aliasDomain *AliasDomain) error { + aliasDomains, err := r.AliasDomains() + if err != nil { + return err + } + + for _, ad := range aliasDomains { + if ad.Name() == aliasDomain.Name() { + return ErrAliasDomainAlreadyExist + } + } + existDomain, err := r.Domain(aliasDomain.Name()) + if err != nil { + return err + } + if existDomain != nil { + return ErrDomainAlreadyExist + } + existDomain, err = r.Domain(aliasDomain.Target()) + if err != nil { + return err + } + if existDomain == nil { + return ErrDomainNotExist + } + + aliasDomains = append(aliasDomains, aliasDomain) + + if err := r.writeAliasDomainsFile(aliasDomains); err != nil { + return err + } + + return nil +} + +// AliasDomainRemove removes a AliasDomain of the input name. +func (r *Repository) AliasDomainRemove(aliasDomainName string) error { + aliasDomains, err := r.AliasDomains() + if err != nil { + return err + } + + idx := -1 + for i, aliasDomain := range aliasDomains { + if aliasDomain.Name() == aliasDomainName { + idx = i + } + } + if idx < 0 { + return ErrAliasDomainNotExist + } + + aliasDomains = append(aliasDomains[:idx], aliasDomains[idx+1:]...) + + if err := r.writeAliasDomainsFile(aliasDomains); err != nil { + return err + } + + return nil +} + +// writeAliasDomainsFile writes a AliasDomain slice to the file. +func (r *Repository) writeAliasDomainsFile(aliasDomains []*AliasDomain) error { + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, FileNameAliasDomains), os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + + sort.Sort(AliasDomainSlice(aliasDomains)) + + for _, aliasDomain := range aliasDomains { + if _, err := fmt.Fprintf(file, "%s:%s\n", aliasDomain.Name(), aliasDomain.Target()); err != nil { + return err + } + } + + return nil +} diff --git a/repository.go b/repository.go index 7ddcb52..b960fb2 100644 --- a/repository.go +++ b/repository.go @@ -21,8 +21,11 @@ var ( ErrDomainNotExist = errors.New("Domain: not exist") ErrDomainAlreadyExist = errors.New("Domain: already exist") ErrDomainIsAliasDomainTarget = errors.New("Domain: is set as alias") - ErrAliasDomainAlreadyExist = errors.New("AliasDomain: already exist") - ErrUserNotExist = errors.New("User: not exist") + + ErrAliasDomainNotExist = errors.New("AliasDomain: not exist") + ErrAliasDomainAlreadyExist = errors.New("AliasDomain: already exist") + + ErrUserNotExist = errors.New("User: not exist") ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") From 5593025c5597141ecf37744cf9d719fe8ffa9131 Mon Sep 17 00:00:00 2001 From: teru Date: Wed, 10 Aug 2016 19:30:05 +0900 Subject: [PATCH 21/39] Implement create/update/remove a User --- repository.go | 6 +- user.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 165 insertions(+), 14 deletions(-) diff --git a/repository.go b/repository.go index b960fb2..6abbd31 100644 --- a/repository.go +++ b/repository.go @@ -25,7 +25,11 @@ var ( ErrAliasDomainNotExist = errors.New("AliasDomain: not exist") ErrAliasDomainAlreadyExist = errors.New("AliasDomain: already exist") - ErrUserNotExist = errors.New("User: not exist") + ErrUserNotExist = errors.New("User: not exist") + ErrUserAlreadyExist = errors.New("User: already exist") + ErrUserIsCatchAllUser = errors.New("User: is set as catchall") + + ErrAliasUserAlreadyExist = errors.New("AliasUser: already exist") ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") diff --git a/user.go b/user.go index b05f4f1..6e59d6c 100644 --- a/user.go +++ b/user.go @@ -2,11 +2,14 @@ package mailfull import ( "bufio" + "fmt" "io/ioutil" "os" "path/filepath" + "sort" "strings" "syscall" + "time" ) // User represents a User. @@ -177,12 +180,8 @@ func (r *Repository) User(domainName, userName string) (*User, error) { // usersHashedPassword returns a string map of usernames to the hashed password. func (r *Repository) usersHashedPassword(domainName string) (map[string]string, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err - } - if domain == nil { - return nil, ErrDomainNotExist + if !validDomainName(domainName) { + return nil, ErrInvalidDomainName } file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword)) @@ -215,14 +214,9 @@ func (r *Repository) usersHashedPassword(domainName string) (map[string]string, // userForwards returns a string slice of forwards that the input name has. func (r *Repository) userForwards(domainName, userName string) ([]string, error) { - domain, err := r.Domain(domainName) - if err != nil { - return nil, err + if !validDomainName(domainName) { + return nil, ErrInvalidDomainName } - if domain == nil { - return nil, ErrDomainNotExist - } - if !validUserName(userName) { return nil, ErrInvalidUserName } @@ -250,3 +244,156 @@ func (r *Repository) userForwards(domainName, userName string) ([]string, error) return forwards, nil } + +// UserCreate creates the input User. +func (r *Repository) UserCreate(domainName string, user *User) error { + existUser, err := r.User(domainName, user.Name()) + if err != nil { + return err + } + if existUser != nil { + return ErrUserAlreadyExist + } + existAliasUser, err := r.AliasUser(domainName, user.Name()) + if err != nil { + return err + } + if existAliasUser != nil { + return ErrAliasUserAlreadyExist + } + + userDirPath := filepath.Join(r.DirMailDataPath, domainName, user.Name()) + + dirNames := []string{ + userDirPath, + filepath.Join(userDirPath, "Maildir"), + filepath.Join(userDirPath, "Maildir/cur"), + filepath.Join(userDirPath, "Maildir/new"), + filepath.Join(userDirPath, "Maildir/tmp"), + } + for _, dirName := range dirNames { + if err := os.Mkdir(dirName, 0777); err != nil { + return err + } + } + + if err := r.UserUpdate(domainName, user); err != nil { + return err + } + + return nil +} + +// UserUpdate updates the input User. +func (r *Repository) UserUpdate(domainName string, user *User) error { + existUser, err := r.User(domainName, user.Name()) + if err != nil { + return err + } + if existUser == nil { + return ErrUserNotExist + } + + hashedPasswords, err := r.usersHashedPassword(domainName) + if err != nil { + return err + } + hashedPasswords[user.Name()] = user.HashedPassword() + if err := r.writeUsersPasswordFile(domainName, hashedPasswords); err != nil { + return err + } + + if err := r.writeUserForwardsFile(domainName, user.Name(), user.Forwards()); err != nil { + return err + } + + return nil +} + +// UserRemove removes a User of the input name. +func (r *Repository) UserRemove(domainName, userName string) error { + existUser, err := r.User(domainName, userName) + if err != nil { + return err + } + if existUser == nil { + return ErrUserNotExist + } + + catchAllUser, err := r.CatchAllUser(domainName) + if err != nil { + return err + } + if catchAllUser != nil && catchAllUser.Name() == userName { + return ErrUserIsCatchAllUser + } + + hashedPasswords, err := r.usersHashedPassword(domainName) + if err != nil { + return err + } + delete(hashedPasswords, userName) + if err := r.writeUsersPasswordFile(domainName, hashedPasswords); err != nil { + return err + } + + userDirPath := filepath.Join(r.DirMailDataPath, domainName, userName) + userBackupDirPath := filepath.Join(r.DirMailDataPath, domainName, "."+userName+".deleted."+time.Now().Format("20060102150405")) + + if err := os.Rename(userDirPath, userBackupDirPath); err != nil { + return err + } + + return nil +} + +// writeUsersPasswordFile writes passwords of each users to the file. +func (r *Repository) writeUsersPasswordFile(domainName string, hashedPasswords map[string]string) error { + if !validDomainName(domainName) { + return ErrInvalidDomainName + } + + keys := make([]string, 0, len(hashedPasswords)) + for key := range hashedPasswords { + keys = append(keys, key) + } + sort.Strings(keys) + + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword), os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + + for _, key := range keys { + if _, err := fmt.Fprintf(file, "%s:%s\n", key, hashedPasswords[key]); err != nil { + return err + } + } + + return nil +} + +// writeUserForwardsFile writes forwards to user's forward file. +func (r *Repository) writeUserForwardsFile(domainName, userName string, forwards []string) error { + if !validDomainName(domainName) { + return ErrInvalidDomainName + } + if !validUserName(userName) { + return ErrInvalidUserName + } + + file, err := os.Create(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) + if err != nil { + return err + } + defer file.Close() + + for _, forward := range forwards { + if _, err := fmt.Fprintf(file, "%s\n", forward); err != nil { + return err + } + } + + return nil +} From a7602a3180a8b5e3d2b95babd21e9b662507a5bd Mon Sep 17 00:00:00 2001 From: teru Date: Wed, 10 Aug 2016 19:45:03 +0900 Subject: [PATCH 22/39] Implement create/update/remove a AliasUser --- aliasuser.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ repository.go | 1 + 2 files changed, 107 insertions(+) diff --git a/aliasuser.go b/aliasuser.go index c5d4966..8389258 100644 --- a/aliasuser.go +++ b/aliasuser.go @@ -3,8 +3,10 @@ package mailfull import ( "bufio" "errors" + "fmt" "os" "path/filepath" + "sort" "strings" ) @@ -137,3 +139,107 @@ func (r *Repository) AliasUser(domainName, aliasUserName string) (*AliasUser, er return nil, nil } + +// AliasUserCreate creates the input AliasUser. +func (r *Repository) AliasUserCreate(domainName string, aliasUser *AliasUser) error { + aliasUsers, err := r.AliasUsers(domainName) + if err != nil { + return err + } + + for _, au := range aliasUsers { + if au.Name() == aliasUser.Name() { + return ErrAliasUserAlreadyExist + } + } + existUser, err := r.User(domainName, aliasUser.Name()) + if err != nil { + return err + } + if existUser != nil { + return ErrUserAlreadyExist + } + + aliasUsers = append(aliasUsers, aliasUser) + + if err := r.writeAliasUsersFile(domainName, aliasUsers); err != nil { + return err + } + + return nil +} + +// AliasUserUpdate updates the input AliasUser. +func (r *Repository) AliasUserUpdate(domainName string, aliasUser *AliasUser) error { + aliasUsers, err := r.AliasUsers(domainName) + if err != nil { + return err + } + + idx := -1 + for i, au := range aliasUsers { + if au.Name() == aliasUser.Name() { + idx = i + } + } + if idx < 0 { + return ErrAliasUserNotExist + } + + aliasUsers[idx] = aliasUser + + if err := r.writeAliasUsersFile(domainName, aliasUsers); err != nil { + return err + } + + return nil +} + +// AliasUserRemove removes a AliasUser of the input name. +func (r *Repository) AliasUserRemove(domainName string, aliasUserName string) error { + aliasUsers, err := r.AliasUsers(domainName) + if err != nil { + return err + } + + idx := -1 + for i, aliasUser := range aliasUsers { + if aliasUser.Name() == aliasUserName { + idx = i + } + } + if idx < 0 { + return ErrAliasUserNotExist + } + + aliasUsers = append(aliasUsers[:idx], aliasUsers[idx+1:]...) + + if err := r.writeAliasUsersFile(domainName, aliasUsers); err != nil { + return err + } + + return nil +} + +// writeAliasUsersFile writes a AliasUser slice to the file. +func (r *Repository) writeAliasUsersFile(domainName string, aliasUsers []*AliasUser) error { + if !validDomainName(domainName) { + return ErrInvalidDomainName + } + + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameAliasUsers), os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + + sort.Sort(AliasUserSlice(aliasUsers)) + + for _, aliasUser := range aliasUsers { + if _, err := fmt.Fprintf(file, "%s:%s\n", aliasUser.Name(), strings.Join(aliasUser.Targets(), ",")); err != nil { + return err + } + } + + return nil +} diff --git a/repository.go b/repository.go index 6abbd31..91532ae 100644 --- a/repository.go +++ b/repository.go @@ -29,6 +29,7 @@ var ( ErrUserAlreadyExist = errors.New("User: already exist") ErrUserIsCatchAllUser = errors.New("User: is set as catchall") + ErrAliasUserNotExist = errors.New("AliasUser: not exist") ErrAliasUserAlreadyExist = errors.New("AliasUser: already exist") ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") From bb0e30ac189c35f75c9780c143bba360cd82fbe5 Mon Sep 17 00:00:00 2001 From: teru Date: Wed, 10 Aug 2016 19:45:23 +0900 Subject: [PATCH 23/39] Implement set/unset a CatchAllUser --- catchalluser.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/catchalluser.go b/catchalluser.go index 8ca2aba..7fa2c9a 100644 --- a/catchalluser.go +++ b/catchalluser.go @@ -2,6 +2,7 @@ package mailfull import ( "bufio" + "fmt" "os" "path/filepath" ) @@ -65,3 +66,45 @@ func (r *Repository) CatchAllUser(domainName string) (*CatchAllUser, error) { return catchAllUser, nil } + +// CatchAllUserSet sets a CatchAllUser to the input Domain. +func (r *Repository) CatchAllUserSet(domainName string, catchAllUser *CatchAllUser) error { + existUser, err := r.User(domainName, catchAllUser.Name()) + if err != nil { + return err + } + if existUser == nil { + return ErrUserNotExist + } + + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser), os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + + if _, err := fmt.Fprintf(file, "%s\n", catchAllUser.Name()); err != nil { + return err + } + + return nil +} + +// CatchAllUserUnset removes a CatchAllUser from the input Domain. +func (r *Repository) CatchAllUserUnset(domainName string) error { + existDomain, err := r.Domain(domainName) + if err != nil { + return err + } + if existDomain == nil { + return ErrDomainNotExist + } + + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser), os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + return err + } + file.Close() + + return nil +} From 42d4e6389ae532caa8b7f2893fd388c90ef33fd0 Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 13:24:01 +0900 Subject: [PATCH 24/39] Change synopsis and help message --- cmd/mailfull/command/commit.go | 7 ++++--- cmd/mailfull/command/domainlist.go | 8 ++++---- cmd/mailfull/command/init.go | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cmd/mailfull/command/commit.go b/cmd/mailfull/command/commit.go index 2bd15d7..790b071 100644 --- a/cmd/mailfull/command/commit.go +++ b/cmd/mailfull/command/commit.go @@ -13,7 +13,7 @@ type CommitCommand struct { // Synopsis returns a one-line synopsis. func (c *CommitCommand) Synopsis() string { - return "Create a database from the structure of the MailData directory." + return "Create databases from the structure of the MailData directory." } // Help returns long-form help text. @@ -23,9 +23,10 @@ Usage: %s %s Description: - Create a database from the structure of the MailData directory. + %s `, - c.CmdName, c.SubCmdName) + c.CmdName, c.SubCmdName, + c.Synopsis()) return txt[1:] } diff --git a/cmd/mailfull/command/domainlist.go b/cmd/mailfull/command/domainlist.go index 11657d2..e8208bb 100644 --- a/cmd/mailfull/command/domainlist.go +++ b/cmd/mailfull/command/domainlist.go @@ -14,7 +14,7 @@ type DomainListCommand struct { // Synopsis returns a one-line synopsis. func (c *DomainListCommand) Synopsis() string { - return "Show all domains." + return "Show domains." } // Help returns long-form help text. @@ -24,9 +24,10 @@ Usage: %s %s Description: - Show all domains. + %s `, - c.CmdName, c.SubCmdName) + c.CmdName, c.SubCmdName, + c.Synopsis()) return txt[1:] } @@ -44,7 +45,6 @@ func (c *DomainListCommand) Run(args []string) int { fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) return 1 } - sort.Sort(mailfull.DomainSlice(domains)) for _, domain := range domains { diff --git a/cmd/mailfull/command/init.go b/cmd/mailfull/command/init.go index 11a818e..b915c7c 100644 --- a/cmd/mailfull/command/init.go +++ b/cmd/mailfull/command/init.go @@ -23,9 +23,10 @@ Usage: %s %s Description: - Initializes current directory as a Mailfull repository. + %s `, - c.CmdName, c.SubCmdName) + c.CmdName, c.SubCmdName, + c.Synopsis()) return txt[1:] } From d7aac2d80393a367bbe4b0a49d9d723cda631e4f Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 13:26:20 +0900 Subject: [PATCH 25/39] Change subcommand name: `domainlist` -> `domains` --- cmd/mailfull/command/{domainlist.go => domains.go} | 10 +++++----- cmd/mailfull/main.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) rename cmd/mailfull/command/{domainlist.go => domains.go} (76%) diff --git a/cmd/mailfull/command/domainlist.go b/cmd/mailfull/command/domains.go similarity index 76% rename from cmd/mailfull/command/domainlist.go rename to cmd/mailfull/command/domains.go index e8208bb..3d7f197 100644 --- a/cmd/mailfull/command/domainlist.go +++ b/cmd/mailfull/command/domains.go @@ -7,18 +7,18 @@ import ( "github.com/directorz/mailfull-go" ) -// DomainListCommand represents a DomainListCommand. -type DomainListCommand struct { +// DomainsCommand represents a DomainsCommand. +type DomainsCommand struct { Meta } // Synopsis returns a one-line synopsis. -func (c *DomainListCommand) Synopsis() string { +func (c *DomainsCommand) Synopsis() string { return "Show domains." } // Help returns long-form help text. -func (c *DomainListCommand) Help() string { +func (c *DomainsCommand) Help() string { txt := fmt.Sprintf(` Usage: %s %s @@ -33,7 +33,7 @@ Description: } // Run runs the command and returns the exit status. -func (c *DomainListCommand) Run(args []string) int { +func (c *DomainsCommand) Run(args []string) int { repo, err := mailfull.OpenRepository(".") if err != nil { fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 9c48eee..711a197 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -43,9 +43,9 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.InitCommand{Meta: meta}, nil }, - "domainlist": func() (cli.Command, error) { + "domains": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() - return &command.DomainListCommand{Meta: meta}, nil + return &command.DomainsCommand{Meta: meta}, nil }, "commit": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() From 73fcb2b25a2278db84d6a08811721fc792340cf5 Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 14:31:17 +0900 Subject: [PATCH 26/39] Remove `Groupname` field --- repository.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/repository.go b/repository.go index 91532ae..2bb2b20 100644 --- a/repository.go +++ b/repository.go @@ -42,7 +42,6 @@ type RepositoryConfig struct { DirDatabasePath string `toml:"dir_database"` DirMailDataPath string `toml:"dir_maildata"` Username string `toml:"username"` - Groupname string `toml:"groupname"` CmdPostalias string `toml:"postalias"` CmdPostmap string `toml:"postmap"` } @@ -75,7 +74,6 @@ func DefaultRepositoryConfig() *RepositoryConfig { DirDatabasePath: "./etc", DirMailDataPath: "./domains", Username: "mailfull", - Groupname: "mailfull", CmdPostalias: "postalias", CmdPostmap: "postmap", } From a12ba9d8ccd8f461c92dffce7a93899ba4db9baa Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 15:14:01 +0900 Subject: [PATCH 27/39] Use current user instead of default parameter --- repository.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/repository.go b/repository.go index 2bb2b20..4be11c2 100644 --- a/repository.go +++ b/repository.go @@ -3,7 +3,9 @@ package mailfull import ( "errors" "os" + "os/user" "path/filepath" + "strconv" "syscall" "github.com/BurntSushi/toml" @@ -73,7 +75,7 @@ func DefaultRepositoryConfig() *RepositoryConfig { c := &RepositoryConfig{ DirDatabasePath: "./etc", DirMailDataPath: "./domains", - Username: "mailfull", + Username: "", CmdPostalias: "postalias", CmdPostmap: "postmap", } @@ -84,12 +86,31 @@ func DefaultRepositoryConfig() *RepositoryConfig { // Repository represents a Repository. type Repository struct { *RepositoryConfig + + uid int + gid int } // NewRepository creates a new Repository instance. func NewRepository(c *RepositoryConfig) (*Repository, error) { + u, err := user.Lookup(c.Username) + if err != nil { + return nil, err + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return nil, err + } + gid, err := strconv.Atoi(u.Gid) + if err != nil { + return nil, err + } + r := &Repository{ RepositoryConfig: c, + + uid: uid, + gid: gid, } return r, nil @@ -201,7 +222,13 @@ func InitRepository(rootPath string) error { } defer configFile.Close() + u, err := user.Current() + if err != nil { + return nil + } + c := DefaultRepositoryConfig() + c.Username = u.Username enc := toml.NewEncoder(configFile) if err := enc.Encode(c); err != nil { From d3b125132be76bd82432716c2b8c905668615660 Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 16:04:25 +0900 Subject: [PATCH 28/39] Set permission/owner/group when creating files or directories #3 --- aliasdomain.go | 2 +- aliasuser.go | 2 +- catchalluser.go | 4 ++-- database.go | 18 ++++++++++++++++++ domain.go | 20 ++++++++++++++++---- repository.go | 4 ++-- user.go | 12 +++++++++--- 7 files changed, 49 insertions(+), 13 deletions(-) diff --git a/aliasdomain.go b/aliasdomain.go index 9728caa..b1fc2f7 100644 --- a/aliasdomain.go +++ b/aliasdomain.go @@ -184,7 +184,7 @@ func (r *Repository) AliasDomainRemove(aliasDomainName string) error { // writeAliasDomainsFile writes a AliasDomain slice to the file. func (r *Repository) writeAliasDomainsFile(aliasDomains []*AliasDomain) error { - file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, FileNameAliasDomains), os.O_RDWR|os.O_TRUNC, 0666) + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, FileNameAliasDomains), os.O_RDWR|os.O_TRUNC, 0600) if err != nil { return err } diff --git a/aliasuser.go b/aliasuser.go index 8389258..647bcfe 100644 --- a/aliasuser.go +++ b/aliasuser.go @@ -227,7 +227,7 @@ func (r *Repository) writeAliasUsersFile(domainName string, aliasUsers []*AliasU return ErrInvalidDomainName } - file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameAliasUsers), os.O_RDWR|os.O_TRUNC, 0666) + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameAliasUsers), os.O_RDWR|os.O_TRUNC, 0600) if err != nil { return err } diff --git a/catchalluser.go b/catchalluser.go index 7fa2c9a..2853afb 100644 --- a/catchalluser.go +++ b/catchalluser.go @@ -77,7 +77,7 @@ func (r *Repository) CatchAllUserSet(domainName string, catchAllUser *CatchAllUs return ErrUserNotExist } - file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser), os.O_RDWR|os.O_TRUNC, 0666) + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser), os.O_RDWR|os.O_TRUNC, 0600) if err != nil { return err } @@ -100,7 +100,7 @@ func (r *Repository) CatchAllUserUnset(domainName string) error { return ErrDomainNotExist } - file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser), os.O_RDWR|os.O_TRUNC, 0666) + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameCatchAllUser), os.O_RDWR|os.O_TRUNC, 0600) if err != nil { return err } diff --git a/database.go b/database.go index 71c02c5..6cc5ad8 100644 --- a/database.go +++ b/database.go @@ -64,6 +64,9 @@ func (r *Repository) generateDbDomains(md *MailData) error { if err != nil { return err } + if err := dbDomains.Chown(r.uid, r.gid); err != nil { + return err + } defer dbDomains.Close() for _, domain := range md.Domains { @@ -86,6 +89,9 @@ func (r *Repository) generateDbDestinations(md *MailData) error { if err != nil { return err } + if err := dbDestinations.Chown(r.uid, r.gid); err != nil { + return err + } defer dbDestinations.Close() for _, domain := range md.Domains { @@ -141,6 +147,9 @@ func (r *Repository) generateDbMaildirs(md *MailData) error { if err != nil { return err } + if err := dbMaildirs.Chown(r.uid, r.gid); err != nil { + return err + } defer dbMaildirs.Close() for _, domain := range md.Domains { @@ -159,6 +168,9 @@ func (r *Repository) generateDbLocaltable(md *MailData) error { if err != nil { return err } + if err := dbLocaltable.Chown(r.uid, r.gid); err != nil { + return err + } defer dbLocaltable.Close() for _, domain := range md.Domains { @@ -180,6 +192,9 @@ func (r *Repository) generateDbForwards(md *MailData) error { if err != nil { return err } + if err := dbForwards.Chown(r.uid, r.gid); err != nil { + return err + } defer dbForwards.Close() for _, domain := range md.Domains { @@ -213,6 +228,9 @@ func (r *Repository) generateDbPasswords(md *MailData) error { if err != nil { return err } + if err := dbPasswords.Chown(r.uid, r.gid); err != nil { + return err + } defer dbPasswords.Close() for _, domain := range md.Domains { diff --git a/domain.go b/domain.go index 1aa23e2..af499e0 100644 --- a/domain.go +++ b/domain.go @@ -116,26 +116,38 @@ func (r *Repository) DomainCreate(domain *Domain) error { domainDirPath := filepath.Join(r.DirMailDataPath, domain.Name()) - if err := os.Mkdir(domainDirPath, 0777); err != nil { + if err := os.Mkdir(domainDirPath, 0700); err != nil { + return err + } + if err := os.Chown(domainDirPath, r.uid, r.gid); err != nil { return err } - usersPasswordFile, err := os.Create(filepath.Join(domainDirPath, FileNameUsersPassword)) + usersPasswordFile, err := os.OpenFile(filepath.Join(domainDirPath, FileNameUsersPassword), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } + if err := usersPasswordFile.Chown(r.uid, r.gid); err != nil { + return err + } usersPasswordFile.Close() - aliasUsersFile, err := os.Create(filepath.Join(domainDirPath, FileNameAliasUsers)) + aliasUsersFile, err := os.OpenFile(filepath.Join(domainDirPath, FileNameAliasUsers), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } + if err := aliasUsersFile.Chown(r.uid, r.gid); err != nil { + return err + } aliasUsersFile.Close() - catchAllUserFile, err := os.Create(filepath.Join(domainDirPath, FileNameCatchAllUser)) + catchAllUserFile, err := os.OpenFile(filepath.Join(domainDirPath, FileNameCatchAllUser), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } + if err := catchAllUserFile.Chown(r.uid, r.gid); err != nil { + return err + } catchAllUserFile.Close() return nil diff --git a/repository.go b/repository.go index 4be11c2..7372956 100644 --- a/repository.go +++ b/repository.go @@ -255,7 +255,7 @@ func InitRepository(rootPath string) error { fi, err = os.Stat(c.DirMailDataPath) if err != nil { if err.(*os.PathError).Err == syscall.ENOENT { - if err = os.Mkdir(c.DirMailDataPath, 0777); err != nil { + if err = os.Mkdir(c.DirMailDataPath, 0700); err != nil { return err } } else { @@ -280,7 +280,7 @@ func InitRepository(rootPath string) error { } } - aliasDomainsFile, err := os.Create(aliasDomainsFileName) + aliasDomainsFile, err := os.OpenFile(aliasDomainsFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return nil } diff --git a/user.go b/user.go index 6e59d6c..cdf3bec 100644 --- a/user.go +++ b/user.go @@ -272,7 +272,10 @@ func (r *Repository) UserCreate(domainName string, user *User) error { filepath.Join(userDirPath, "Maildir/tmp"), } for _, dirName := range dirNames { - if err := os.Mkdir(dirName, 0777); err != nil { + if err := os.Mkdir(dirName, 0700); err != nil { + return err + } + if err := os.Chown(dirName, r.uid, r.gid); err != nil { return err } } @@ -359,7 +362,7 @@ func (r *Repository) writeUsersPasswordFile(domainName string, hashedPasswords m } sort.Strings(keys) - file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword), os.O_RDWR|os.O_TRUNC, 0666) + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword), os.O_RDWR|os.O_TRUNC, 0600) if err != nil { return err } @@ -383,10 +386,13 @@ func (r *Repository) writeUserForwardsFile(domainName, userName string, forwards return ErrInvalidUserName } - file, err := os.Create(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards)) + file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } + if err := file.Chown(r.uid, r.gid); err != nil { + return err + } defer file.Close() for _, forward := range forwards { From 2ec3e444a3c7cfe2e5ec9a07362fd6b7e6ac3066 Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 17:36:28 +0900 Subject: [PATCH 29/39] Implement subcommands: `domainadd`, `domaindel` --- cmd/mailfull/command/domainadd.go | 88 +++++++++++++++++++++++++++++++ cmd/mailfull/command/domaindel.go | 71 +++++++++++++++++++++++++ cmd/mailfull/main.go | 8 +++ 3 files changed, 167 insertions(+) create mode 100644 cmd/mailfull/command/domainadd.go create mode 100644 cmd/mailfull/command/domaindel.go diff --git a/cmd/mailfull/command/domainadd.go b/cmd/mailfull/command/domainadd.go new file mode 100644 index 0000000..8af48d8 --- /dev/null +++ b/cmd/mailfull/command/domainadd.go @@ -0,0 +1,88 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// DomainAddCommand represents a DomainAddCommand. +type DomainAddCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *DomainAddCommand) Synopsis() string { + return "Create a new domain and postmaster." +} + +// Help returns long-form help text. +func (c *DomainAddCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name that you want to create. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *DomainAddCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + domainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + domain, err := mailfull.NewDomain(domainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.DomainCreate(domain); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + user, err := mailfull.NewUser("postmaster", "", nil) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.UserCreate(domainName, user); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/domaindel.go b/cmd/mailfull/command/domaindel.go new file mode 100644 index 0000000..e7dcee7 --- /dev/null +++ b/cmd/mailfull/command/domaindel.go @@ -0,0 +1,71 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// DomainDelCommand represents a DomainDelCommand. +type DomainDelCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *DomainDelCommand) Synopsis() string { + return "Delete and backup a domain." +} + +// Help returns long-form help text. +func (c *DomainDelCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name that you want to delete. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *DomainDelCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + domainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.DomainRemove(domainName); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 711a197..ce03c5c 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -47,6 +47,14 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.DomainsCommand{Meta: meta}, nil }, + "domainadd": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.DomainAddCommand{Meta: meta}, nil + }, + "domaindel": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.DomainDelCommand{Meta: meta}, nil + }, "commit": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.CommitCommand{Meta: meta}, nil From 2bcfc6b0fee7b3d8de70143450d09b929cb90dbb Mon Sep 17 00:00:00 2001 From: teru Date: Thu, 11 Aug 2016 17:42:17 +0900 Subject: [PATCH 30/39] Use a default password hash --- cmd/mailfull/command/domainadd.go | 2 +- const.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/mailfull/command/domainadd.go b/cmd/mailfull/command/domainadd.go index 8af48d8..6e04fb2 100644 --- a/cmd/mailfull/command/domainadd.go +++ b/cmd/mailfull/command/domainadd.go @@ -61,7 +61,7 @@ func (c *DomainAddCommand) Run(args []string) int { return 1 } - user, err := mailfull.NewUser("postmaster", "", nil) + user, err := mailfull.NewUser("postmaster", mailfull.NeverMatchHashedPassword, nil) if err != nil { fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) return 1 diff --git a/const.go b/const.go index 07beaa3..2cf9aa6 100644 --- a/const.go +++ b/const.go @@ -18,3 +18,6 @@ const ( FileNameDbForwards = "forwards" FileNameDbPasswords = "vpasswd" ) + +// NeverMatchHashedPassword is hash string that is never match with any password. +const NeverMatchHashedPassword = "{SSHA}!!" From 5e13d9546c10be08e72d63f6c54a929a3fd132a3 Mon Sep 17 00:00:00 2001 From: teru Date: Fri, 12 Aug 2016 20:35:46 +0900 Subject: [PATCH 31/39] Implement subcommands: `aliasdomains`, `aliasdomainadd`, `aliasdomaindel` --- cmd/mailfull/command/aliasdomainadd.go | 80 ++++++++++++++++++++++++++ cmd/mailfull/command/aliasdomaindel.go | 71 +++++++++++++++++++++++ cmd/mailfull/command/aliasdomains.go | 75 ++++++++++++++++++++++++ cmd/mailfull/main.go | 12 ++++ 4 files changed, 238 insertions(+) create mode 100644 cmd/mailfull/command/aliasdomainadd.go create mode 100644 cmd/mailfull/command/aliasdomaindel.go create mode 100644 cmd/mailfull/command/aliasdomains.go diff --git a/cmd/mailfull/command/aliasdomainadd.go b/cmd/mailfull/command/aliasdomainadd.go new file mode 100644 index 0000000..41d1222 --- /dev/null +++ b/cmd/mailfull/command/aliasdomainadd.go @@ -0,0 +1,80 @@ +package command + +import ( + "fmt" + + mailfull "github.com/directorz/mailfull-go" +) + +// AliasDomainAddCommand represents a AliasDomainAddCommand. +type AliasDomainAddCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasDomainAddCommand) Synopsis() string { + return "Create a new aliasdomain." +} + +// Help returns long-form help text. +func (c *AliasDomainAddCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain target + +Description: + %s + +Required Args: + domain + The domain name that you want to create. + target + The target domain name. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasDomainAddCommand) Run(args []string) int { + if len(args) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + aliasDomainName := args[0] + targetDomainName := args[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + aliasDomain, err := mailfull.NewAliasDomain(aliasDomainName, targetDomainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.AliasDomainCreate(aliasDomain); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/aliasdomaindel.go b/cmd/mailfull/command/aliasdomaindel.go new file mode 100644 index 0000000..a16411c --- /dev/null +++ b/cmd/mailfull/command/aliasdomaindel.go @@ -0,0 +1,71 @@ +package command + +import ( + "fmt" + + mailfull "github.com/directorz/mailfull-go" +) + +// AliasDomainDelCommand represents a AliasDomainDelCommand. +type AliasDomainDelCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasDomainDelCommand) Synopsis() string { + return "Delete a aliasdomain." +} + +// Help returns long-form help text. +func (c *AliasDomainDelCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name that you want to delete. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasDomainDelCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + aliasDomainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.AliasDomainRemove(aliasDomainName); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/aliasdomains.go b/cmd/mailfull/command/aliasdomains.go new file mode 100644 index 0000000..62cbac7 --- /dev/null +++ b/cmd/mailfull/command/aliasdomains.go @@ -0,0 +1,75 @@ +package command + +import ( + "fmt" + "sort" + + mailfull "github.com/directorz/mailfull-go" +) + +// AliasDomainsCommand represents a AliasDomainsCommand. +type AliasDomainsCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasDomainsCommand) Synopsis() string { + return "Show aliasdomains." +} + +// Help returns long-form help text. +func (c *AliasDomainsCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s [domain] + +Description: + %s + +Optional Args: + domain + Show aliasdomains that the target is "domain". +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasDomainsCommand) Run(args []string) int { + if len(args) > 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + targetDomainName := "" + if len(args) == 1 { + targetDomainName = args[0] + } + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + aliasDomains, err := repo.AliasDomains() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + sort.Sort(mailfull.AliasDomainSlice(aliasDomains)) + + for _, aliasDomain := range aliasDomains { + if targetDomainName != "" { + if aliasDomain.Target() == targetDomainName { + fmt.Fprintf(c.UI.Writer, "%s\n", aliasDomain.Name()) + } + } else { + fmt.Fprintf(c.UI.Writer, "%s\n", aliasDomain.Name()) + } + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index ce03c5c..f67cef6 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -55,6 +55,18 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.DomainDelCommand{Meta: meta}, nil }, + "aliasdomains": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasDomainsCommand{Meta: meta}, nil + }, + "aliasdomainadd": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasDomainAddCommand{Meta: meta}, nil + }, + "aliasdomaindel": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasDomainDelCommand{Meta: meta}, nil + }, "commit": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.CommitCommand{Meta: meta}, nil From ec5347b2f226f742dac6c01a43d84d41e5026574 Mon Sep 17 00:00:00 2001 From: teru Date: Sat, 13 Aug 2016 08:02:46 +0900 Subject: [PATCH 32/39] Implement subcommands: `users`, `useradd`, `userdel` --- cmd/mailfull/command/useradd.go | 86 +++++++++++++++++++++++++++++++++ cmd/mailfull/command/userdel.go | 80 ++++++++++++++++++++++++++++++ cmd/mailfull/command/users.go | 66 +++++++++++++++++++++++++ cmd/mailfull/main.go | 12 +++++ 4 files changed, 244 insertions(+) create mode 100644 cmd/mailfull/command/useradd.go create mode 100644 cmd/mailfull/command/userdel.go create mode 100644 cmd/mailfull/command/users.go diff --git a/cmd/mailfull/command/useradd.go b/cmd/mailfull/command/useradd.go new file mode 100644 index 0000000..8f3bd1f --- /dev/null +++ b/cmd/mailfull/command/useradd.go @@ -0,0 +1,86 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/directorz/mailfull-go" +) + +// UserAddCommand represents a UserAddCommand. +type UserAddCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *UserAddCommand) Synopsis() string { + return "Create a new user." +} + +// Help returns long-form help text. +func (c *UserAddCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address + +Description: + %s + +Required Args: + address + The email address that you want to create. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *UserAddCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + userName := words[0] + domainName := words[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + user, err := mailfull.NewUser(userName, mailfull.NeverMatchHashedPassword, nil) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.UserCreate(domainName, user); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/userdel.go b/cmd/mailfull/command/userdel.go new file mode 100644 index 0000000..b19c365 --- /dev/null +++ b/cmd/mailfull/command/userdel.go @@ -0,0 +1,80 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/directorz/mailfull-go" +) + +// UserDelCommand represents a UserDelCommand. +type UserDelCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *UserDelCommand) Synopsis() string { + return "Delete and backup a user." +} + +// Help returns long-form help text. +func (c *UserDelCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address + +Description: + %s + +Required Args: + address + The email address that you want to delete. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *UserDelCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + userName := words[0] + domainName := words[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.UserRemove(domainName, userName); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/users.go b/cmd/mailfull/command/users.go new file mode 100644 index 0000000..2f5038d --- /dev/null +++ b/cmd/mailfull/command/users.go @@ -0,0 +1,66 @@ +package command + +import ( + "fmt" + "sort" + + "github.com/directorz/mailfull-go" +) + +// UsersCommand represents a UsersCommand. +type UsersCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *UsersCommand) Synopsis() string { + return "Show users." +} + +// Help returns long-form help text. +func (c *UsersCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *UsersCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + targetDomainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + users, err := repo.Users(targetDomainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + sort.Sort(mailfull.UserSlice(users)) + + for _, user := range users { + fmt.Fprintf(c.UI.Writer, "%s\n", user.Name()) + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index f67cef6..5f61a5d 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -67,6 +67,18 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.AliasDomainDelCommand{Meta: meta}, nil }, + "users": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.UsersCommand{Meta: meta}, nil + }, + "useradd": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.UserAddCommand{Meta: meta}, nil + }, + "userdel": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.UserDelCommand{Meta: meta}, nil + }, "commit": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.CommitCommand{Meta: meta}, nil From 0e5c6715807833e50b5db681a8ce86afe31d99b0 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 14 Aug 2016 07:55:14 +0900 Subject: [PATCH 33/39] Implement subcommands: `userpasswd`, `usercheckpw` --- cmd/mailfull/command/usercheckpw.go | 100 +++++++++++++++++++++ cmd/mailfull/command/userpasswd.go | 131 ++++++++++++++++++++++++++++ cmd/mailfull/main.go | 8 ++ 3 files changed, 239 insertions(+) create mode 100644 cmd/mailfull/command/usercheckpw.go create mode 100644 cmd/mailfull/command/userpasswd.go diff --git a/cmd/mailfull/command/usercheckpw.go b/cmd/mailfull/command/usercheckpw.go new file mode 100644 index 0000000..6fd8e37 --- /dev/null +++ b/cmd/mailfull/command/usercheckpw.go @@ -0,0 +1,100 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/directorz/mailfull-go" + "github.com/jsimonetti/pwscheme/ssha" +) + +// UserCheckPwCommand represents a UserCheckPwCommand. +type UserCheckPwCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *UserCheckPwCommand) Synopsis() string { + return "Check user's password." +} + +// Help returns long-form help text. +func (c *UserCheckPwCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address [password] + +Description: + %s + +Required Args: + address + The email address that you want to check the password. + +Optional Args: + password + Specify the password instead of your typing. + This option is not recommended because the password will be visible in your shell history. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *UserCheckPwCommand) Run(args []string) int { + if len(args) != 1 && len(args) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + userName := words[0] + domainName := words[1] + + rawPassword := "" + if len(args) == 2 { + rawPassword = args[1] + } + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + user, err := repo.User(domainName, userName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + if user == nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", mailfull.ErrUserNotExist) + return 1 + } + + if len(args) != 2 { + input, err1 := c.UI.AskSecret(fmt.Sprintf("Enter password for %s:", address)) + if err1 != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err1) + return 1 + } + + rawPassword = input + } + + if ok, _ := ssha.Validate(rawPassword, user.HashedPassword()); !ok { + fmt.Fprintf(c.UI.Writer, "The password you entered is incorrect.\n") + return 1 + } + + fmt.Fprintf(c.UI.Writer, "The password you entered is correct.\n") + return 0 +} diff --git a/cmd/mailfull/command/userpasswd.go b/cmd/mailfull/command/userpasswd.go new file mode 100644 index 0000000..5f5be57 --- /dev/null +++ b/cmd/mailfull/command/userpasswd.go @@ -0,0 +1,131 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/directorz/mailfull-go" + "github.com/jsimonetti/pwscheme/ssha" +) + +// UserPasswdCommand represents a UserPasswdCommand. +type UserPasswdCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *UserPasswdCommand) Synopsis() string { + return "Update user's password." +} + +// Help returns long-form help text. +func (c *UserPasswdCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address [password] + +Description: + %s + +Required Args: + address + The email address that you want to update the password. + +Optional Args: + password + Specify the password instead of your typing. + This option is not recommended because the password will be visible in your shell history. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *UserPasswdCommand) Run(args []string) int { + if len(args) != 1 && len(args) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + userName := words[0] + domainName := words[1] + + rawPassword := "" + if len(args) == 2 { + rawPassword = args[1] + } + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + user, err := repo.User(domainName, userName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + if user == nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", mailfull.ErrUserNotExist) + return 1 + } + + if len(args) != 2 { + input1, err1 := c.UI.AskSecret(fmt.Sprintf("Enter new password for %s:", address)) + if err1 != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err1) + return 1 + } + input2, err2 := c.UI.AskSecret("Retype new password:") + if err2 != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err2) + return 1 + } + if input1 != input2 { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] inputs do not match.\n") + return 1 + } + rawPassword = input1 + } + + hashedPassword := mailfull.NeverMatchHashedPassword + if rawPassword != "" { + str, errHash := ssha.Generate(rawPassword, 4) + if errHash != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", errHash) + return 1 + } + hashedPassword = str + } + + user.SetHashedPassword(hashedPassword) + + if err := repo.UserUpdate(domainName, user); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 5f61a5d..5d2cad0 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -79,6 +79,14 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.UserDelCommand{Meta: meta}, nil }, + "userpasswd": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.UserPasswdCommand{Meta: meta}, nil + }, + "usercheckpw": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.UserCheckPwCommand{Meta: meta}, nil + }, "commit": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.CommitCommand{Meta: meta}, nil From fdd476d3b78be501c5cc81a24c07842cda14e1ef Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 14 Aug 2016 12:30:24 +0900 Subject: [PATCH 34/39] Implement subcommands: `catchall`, `catchallset`, `catchallunset` --- cmd/mailfull/command/catchall.go | 64 +++++++++++++++++++++ cmd/mailfull/command/catchallset.go | 80 +++++++++++++++++++++++++++ cmd/mailfull/command/catchallunset.go | 71 ++++++++++++++++++++++++ cmd/mailfull/main.go | 12 ++++ 4 files changed, 227 insertions(+) create mode 100644 cmd/mailfull/command/catchall.go create mode 100644 cmd/mailfull/command/catchallset.go create mode 100644 cmd/mailfull/command/catchallunset.go diff --git a/cmd/mailfull/command/catchall.go b/cmd/mailfull/command/catchall.go new file mode 100644 index 0000000..3c1db48 --- /dev/null +++ b/cmd/mailfull/command/catchall.go @@ -0,0 +1,64 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// CatchAllCommand represents a CatchAllCommand. +type CatchAllCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *CatchAllCommand) Synopsis() string { + return "Show a catchall user." +} + +// Help returns long-form help text. +func (c *CatchAllCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *CatchAllCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + domainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + catchAllUser, err := repo.CatchAllUser(domainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if catchAllUser != nil { + fmt.Fprintf(c.UI.Writer, "%s\n", catchAllUser.Name()) + } + + return 0 +} diff --git a/cmd/mailfull/command/catchallset.go b/cmd/mailfull/command/catchallset.go new file mode 100644 index 0000000..c7fa893 --- /dev/null +++ b/cmd/mailfull/command/catchallset.go @@ -0,0 +1,80 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// CatchAllSetCommand represents a CatchAllSetCommand. +type CatchAllSetCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *CatchAllSetCommand) Synopsis() string { + return "Set a catchall user." +} + +// Help returns long-form help text. +func (c *CatchAllSetCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain user + +Description: + %s + +Required Args: + domain + The domain name. + user + The user name that you want to set as catchall user. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *CatchAllSetCommand) Run(args []string) int { + if len(args) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + domainName := args[0] + userName := args[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + catchAllUser, err := mailfull.NewCatchAllUser(userName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.CatchAllUserSet(domainName, catchAllUser); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/catchallunset.go b/cmd/mailfull/command/catchallunset.go new file mode 100644 index 0000000..bf4164a --- /dev/null +++ b/cmd/mailfull/command/catchallunset.go @@ -0,0 +1,71 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// CatchAllUnsetCommand represents a CatchAllUnsetCommand. +type CatchAllUnsetCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *CatchAllUnsetCommand) Synopsis() string { + return "Unset a catchall user." +} + +// Help returns long-form help text. +func (c *CatchAllUnsetCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *CatchAllUnsetCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + domainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.CatchAllUserUnset(domainName); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 5d2cad0..b05b11d 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -87,6 +87,18 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.UserCheckPwCommand{Meta: meta}, nil }, + "catchall": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.CatchAllCommand{Meta: meta}, nil + }, + "catchallset": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.CatchAllSetCommand{Meta: meta}, nil + }, + "catchallunset": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.CatchAllUnsetCommand{Meta: meta}, nil + }, "commit": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.CommitCommand{Meta: meta}, nil From cd1101d096753a363d96a38ee5f5e46d9af06fc2 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 14 Aug 2016 12:31:29 +0900 Subject: [PATCH 35/39] Implement subcommands: `aliasusers`, `aliasuseradd`, `aliasusermod`, `aliasuserdel` --- cmd/mailfull/command/aliasuseradd.go | 89 +++++++++++++++++++++++++ cmd/mailfull/command/aliasuserdel.go | 79 ++++++++++++++++++++++ cmd/mailfull/command/aliasusermod.go | 98 ++++++++++++++++++++++++++++ cmd/mailfull/command/aliasusers.go | 66 +++++++++++++++++++ cmd/mailfull/main.go | 16 +++++ 5 files changed, 348 insertions(+) create mode 100644 cmd/mailfull/command/aliasuseradd.go create mode 100644 cmd/mailfull/command/aliasuserdel.go create mode 100644 cmd/mailfull/command/aliasusermod.go create mode 100644 cmd/mailfull/command/aliasusers.go diff --git a/cmd/mailfull/command/aliasuseradd.go b/cmd/mailfull/command/aliasuseradd.go new file mode 100644 index 0000000..815a01a --- /dev/null +++ b/cmd/mailfull/command/aliasuseradd.go @@ -0,0 +1,89 @@ +package command + +import ( + "fmt" + "strings" + + mailfull "github.com/directorz/mailfull-go" +) + +// AliasUserAddCommand represents a AliasUserAddCommand. +type AliasUserAddCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasUserAddCommand) Synopsis() string { + return "Create a new aliasuser." +} + +// Help returns long-form help text. +func (c *AliasUserAddCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address target [target...] + +Description: + %s + +Required Args: + address + The email address that you want to create. + target + Target email addresses. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasUserAddCommand) Run(args []string) int { + if len(args) < 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + targets := args[1:] + + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + aliasUserName := words[0] + domainName := words[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + aliasUser, err := mailfull.NewAliasUser(aliasUserName, targets) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.AliasUserCreate(domainName, aliasUser); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/aliasuserdel.go b/cmd/mailfull/command/aliasuserdel.go new file mode 100644 index 0000000..ea519c2 --- /dev/null +++ b/cmd/mailfull/command/aliasuserdel.go @@ -0,0 +1,79 @@ +package command + +import ( + "fmt" + "strings" + + mailfull "github.com/directorz/mailfull-go" +) + +// AliasUserDelCommand represents a AliasUserDelCommand. +type AliasUserDelCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasUserDelCommand) Synopsis() string { + return "Delete a aliasuser." +} + +// Help returns long-form help text. +func (c *AliasUserDelCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address + +Description: + %s + +Required Args: + address + The email address that you want to delete. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasUserDelCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + aliasUserName := words[0] + domainName := words[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.AliasUserRemove(domainName, aliasUserName); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/aliasusermod.go b/cmd/mailfull/command/aliasusermod.go new file mode 100644 index 0000000..16763fe --- /dev/null +++ b/cmd/mailfull/command/aliasusermod.go @@ -0,0 +1,98 @@ +package command + +import ( + "fmt" + "strings" + + mailfull "github.com/directorz/mailfull-go" +) + +// AliasUserModCommand represents a AliasUserModCommand. +type AliasUserModCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasUserModCommand) Synopsis() string { + return "Modify a aliasuser." +} + +// Help returns long-form help text. +func (c *AliasUserModCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s address target [target...] + +Description: + %s + +Required Args: + address + The email address that you want to modify. + target + Target email addresses. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasUserModCommand) Run(args []string) int { + if len(args) < 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + address := args[0] + targets := args[1:] + + words := strings.Split(address, "@") + if len(words) != 2 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + aliasUserName := words[0] + domainName := words[1] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + aliasUser, err := repo.AliasUser(domainName, aliasUserName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + if aliasUser == nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", mailfull.ErrAliasUserNotExist) + return 1 + } + + if err := aliasUser.SetTargets(targets); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + if err := repo.AliasUserUpdate(domainName, aliasUser); err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + mailData, err := repo.MailData() + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + err = repo.GenerateDatabases(mailData) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/command/aliasusers.go b/cmd/mailfull/command/aliasusers.go new file mode 100644 index 0000000..036b23a --- /dev/null +++ b/cmd/mailfull/command/aliasusers.go @@ -0,0 +1,66 @@ +package command + +import ( + "fmt" + "sort" + + "github.com/directorz/mailfull-go" +) + +// AliasUsersCommand represents a AliasUsersCommand. +type AliasUsersCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *AliasUsersCommand) Synopsis() string { + return "Show aliasusers." +} + +// Help returns long-form help text. +func (c *AliasUsersCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *AliasUsersCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + targetDomainName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + aliasUsers, err := repo.AliasUsers(targetDomainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + sort.Sort(mailfull.AliasUserSlice(aliasUsers)) + + for _, aliasUser := range aliasUsers { + fmt.Fprintf(c.UI.Writer, "%s\n", aliasUser.Name()) + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index b05b11d..a0fd7a2 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -87,6 +87,22 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.UserCheckPwCommand{Meta: meta}, nil }, + "aliasusers": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasUsersCommand{Meta: meta}, nil + }, + "aliasuseradd": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasUserAddCommand{Meta: meta}, nil + }, + "aliasusermod": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasUserModCommand{Meta: meta}, nil + }, + "aliasuserdel": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.AliasUserDelCommand{Meta: meta}, nil + }, "catchall": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.CatchAllCommand{Meta: meta}, nil From 3751caa9a5d2a7a3669d089912e1504bafd6cf3f Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 14 Aug 2016 13:03:32 +0900 Subject: [PATCH 36/39] Implement to generate configuration for postfix/dovecot #6 --- cmd/mailfull/command/genconfig.go | 67 ++++++++++++++ cmd/mailfull/main.go | 4 + generateconfig.go | 140 ++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 cmd/mailfull/command/genconfig.go create mode 100644 generateconfig.go diff --git a/cmd/mailfull/command/genconfig.go b/cmd/mailfull/command/genconfig.go new file mode 100644 index 0000000..e2a4b27 --- /dev/null +++ b/cmd/mailfull/command/genconfig.go @@ -0,0 +1,67 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// GenConfigCommand represents a GenConfigCommand. +type GenConfigCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *GenConfigCommand) Synopsis() string { + return "Write a Postfix or Dovecot configuration to stdout." +} + +// Help returns long-form help text. +func (c *GenConfigCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s name + +Description: + %s + +Required Args: + name + The software name that you want to generate a configuration. + Available names are "postfix" and "dovecot". +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *GenConfigCommand) Run(args []string) int { + if len(args) != 1 { + fmt.Fprintf(c.UI.ErrorWriter, "%v\n", c.Help()) + return 1 + } + + softwareName := args[0] + + repo, err := mailfull.OpenRepository(".") + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + + switch softwareName { + case "postfix": + fmt.Fprintf(c.UI.Writer, "%s", repo.GenerateConfigPostfix()) + + case "dovecot": + fmt.Fprintf(c.UI.Writer, "%s", repo.GenerateConfigDovecot()) + + default: + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] Specify \"postfix\" or \"dovecot\".\n") + return 1 + } + + return 0 +} diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index a0fd7a2..86c3d9a 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -43,6 +43,10 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.InitCommand{Meta: meta}, nil }, + "genconfig": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.GenConfigCommand{Meta: meta}, nil + }, "domains": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.DomainsCommand{Meta: meta}, nil diff --git a/generateconfig.go b/generateconfig.go new file mode 100644 index 0000000..a2cc75f --- /dev/null +++ b/generateconfig.go @@ -0,0 +1,140 @@ +package mailfull + +import ( + "fmt" + "path/filepath" + "time" +) + +// GenerateConfigPostfix generate a configuration for Postfix. +func (r *Repository) GenerateConfigPostfix() string { + cfg := fmt.Sprintf(` +# +# Sample configuration: main.cf +# Generated by mailfull %s on %s +# + +#myhostname = host.example.com +mydomain = $myhostname +myorigin = $mydomain + +inet_interfaces = all +mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain +mynetworks_style = host +recipient_delimiter = - +message_size_limit = 10240000 +mailbox_size_limit = 51200000 +virtual_mailbox_limit = 51200000 + +virtual_mailbox_domains = hash:%s +virtual_mailbox_base = %s +virtual_mailbox_maps = hash:%s +virtual_uid_maps = static:%d +virtual_gid_maps = static:%d +virtual_alias_maps = hash:%s +transport_maps = regexp:%s +alias_maps = hash:/etc/aliases, hash:%s +alias_database = hash:/etc/aliases, hash:%s + +smtpd_sasl_auth_enable = yes +smtpd_sasl_local_domain = $myhostname +smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination +smtpd_sasl_type = dovecot +smtpd_sasl_path = private/auth + +smtpd_tls_cert_file = /etc/pki/dovecot/certs/dovecot.pem +smtpd_tls_key_file = /etc/pki/dovecot/private/dovecot.pem +#smtpd_tls_CAfile = +smtpd_tls_session_cache_database = btree:/etc/postfix/smtpd_scache +smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3 + +smtp_tls_security_level = may +smtp_tls_loglevel = 1 +smtpd_tls_security_level = may +smtpd_tls_loglevel = 1 +`, + Version, time.Now().Format(time.RFC3339), + filepath.Join(r.DirDatabasePath, FileNameDbDomains), + r.DirMailDataPath, + filepath.Join(r.DirDatabasePath, FileNameDbMaildirs), + r.uid, + r.gid, + filepath.Join(r.DirDatabasePath, FileNameDbDestinations), + filepath.Join(r.DirDatabasePath, FileNameDbLocaltable), + filepath.Join(r.DirDatabasePath, FileNameDbForwards), + filepath.Join(r.DirDatabasePath, FileNameDbForwards), + ) + + return cfg[1:] +} + +// GenerateConfigDovecot generate a configuration for Dovecot. +func (r *Repository) GenerateConfigDovecot() string { + cfg := fmt.Sprintf(` +# +# Sample configuration: dovecot.conf +# Generated by mailfull %s on %s +# + +protocols = imap pop3 +auth_mechanisms = plain login +mail_location = maildir:~/Maildir + +ssl = yes +ssl_cert = Date: Sun, 14 Aug 2016 13:24:02 +0900 Subject: [PATCH 37/39] Add package overviews for godoc #5 --- cmd/mailfull/main.go | 3 +++ mailfull.go | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 mailfull.go diff --git a/cmd/mailfull/main.go b/cmd/mailfull/main.go index 86c3d9a..83154d7 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/main.go @@ -1,3 +1,6 @@ +/* +Command mailfull is a CLI application using the mailfull package. +*/ package main import ( diff --git a/mailfull.go b/mailfull.go new file mode 100644 index 0000000..86cb38a --- /dev/null +++ b/mailfull.go @@ -0,0 +1,4 @@ +/* +Package mailfull contains operations for a mailfull repository. +*/ +package mailfull From 51d3d82a5446c8dbd3ca80f60e69addca407a625 Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 15 Aug 2016 14:48:17 +0900 Subject: [PATCH 38/39] Write README.md --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1151d6f..3106b9c 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,64 @@ mailfull-go A management tool for virtual domain email for Postfix and Dovecot written in Go. -**Currently in development.** +Features +-------- + +- You can use both virtual users and system users. +- Mailfull does not involve in delivery processes of the MTA, is only to generate configuration databases. +- You do not need to restart Postfix/Dovecot to apply configuration databases. +- The received email can be passed to the programs. + +Installation +------------ + +### go get + +Installed in `$GOPATH/bin` + +``` +$ go get github.com/directorz/mailfull-go/cmd/mailfull +``` + +Quick Start +----------- + +Create a new user for Mailfull. + +``` +# useradd -r -s /bin/bash mailfull +# su - mailfull +``` + +Initialize a directory as a Mailfull repository. + +``` +$ mkdir /path/to/repo && cd /path/to/repo +$ mailfull init +``` + +Generate configurations for Postfix and Dovecot. (Edit as needed.) + +``` +$ mailfull genconfig postfix > /etc/postfix/main.cf +$ mailfull genconfig dovecot > /etc/dovecot/dovecot.conf +``` + +Start Postfix and Dovecot. + +``` +# systemctl start postfix.service +# systemctl start dovecot.service +``` + +Add a new domain and user. + +``` +# cd /path/to/repo + +# mailfull domainadd example.com +# mailfull useradd hoge@example.com +# mailfull userpasswd hoge@example.com +``` + +Enjoy! From 5c0cc00251d0b92471fdf44837ec2d515bdd0916 Mon Sep 17 00:00:00 2001 From: teru Date: Mon, 15 Aug 2016 14:49:34 +0900 Subject: [PATCH 39/39] Bump version to 0.0.1 --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index d2f4db1..2d2cf0d 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package mailfull // Version is a version number. -const Version = "0.0.0" +const Version = "0.0.1"