From 7cac94f0f3e6d31b6b164dc223de36eb94e33256 Mon Sep 17 00:00:00 2001 From: teru Date: Sun, 31 Jul 2016 10:06:30 +0900 Subject: [PATCH] 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"