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/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! diff --git a/aliasdomain.go b/aliasdomain.go new file mode 100644 index 0000000..b1fc2f7 --- /dev/null +++ b/aliasdomain.go @@ -0,0 +1,202 @@ +package mailfull + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// AliasDomain represents a AliasDomain. +type AliasDomain struct { + name string + 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) { + ad := &AliasDomain{} + + if err := ad.setName(name); err != nil { + return nil, err + } + + 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 +} + +// 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 + } + defer file.Close() + + 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 +} + +// 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, 0600) + 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/aliasuser.go b/aliasuser.go new file mode 100644 index 0000000..647bcfe --- /dev/null +++ b/aliasuser.go @@ -0,0 +1,245 @@ +package mailfull + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// Errors for parameter. +var ( + ErrNotEnoughAliasUserTargets = errors.New("AliasUser: targets not enough") +) + +// AliasUser represents a AliasUser. +type AliasUser struct { + name string + 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) { + au := &AliasUser{} + + if err := au.setName(name); err != nil { + return nil, err + } + + 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 +} + +// 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 + } + defer file.Close() + + 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 +} + +// 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, 0600) + 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/catchalluser.go b/catchalluser.go new file mode 100644 index 0000000..2853afb --- /dev/null +++ b/catchalluser.go @@ -0,0 +1,110 @@ +package mailfull + +import ( + "bufio" + "fmt" + "os" + "path/filepath" +) + +// 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 +} + +// 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 + } + defer file.Close() + + 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 +} + +// 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, 0600) + 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, 0600) + if err != nil { + return err + } + file.Close() + + return nil +} 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/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/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/command/commit.go b/cmd/mailfull/command/commit.go new file mode 100644 index 0000000..790b071 --- /dev/null +++ b/cmd/mailfull/command/commit.go @@ -0,0 +1,55 @@ +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 databases 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: + %s +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + 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/command/domainadd.go b/cmd/mailfull/command/domainadd.go new file mode 100644 index 0000000..6e04fb2 --- /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", 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/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/command/domains.go b/cmd/mailfull/command/domains.go new file mode 100644 index 0000000..3d7f197 --- /dev/null +++ b/cmd/mailfull/command/domains.go @@ -0,0 +1,55 @@ +package command + +import ( + "fmt" + "sort" + + "github.com/directorz/mailfull-go" +) + +// DomainsCommand represents a DomainsCommand. +type DomainsCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *DomainsCommand) Synopsis() string { + return "Show domains." +} + +// Help returns long-form help text. +func (c *DomainsCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s + +Description: + %s +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *DomainsCommand) 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/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/command/init.go b/cmd/mailfull/command/init.go new file mode 100644 index 0000000..b915c7c --- /dev/null +++ b/cmd/mailfull/command/init.go @@ -0,0 +1,42 @@ +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: + %s +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + 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/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/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/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/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/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 new file mode 100644 index 0000000..83154d7 --- /dev/null +++ b/cmd/mailfull/main.go @@ -0,0 +1,137 @@ +/* +Command mailfull is a CLI application using the mailfull package. +*/ +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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + } + + exitCode, err := c.Run() + if err != nil { + fmt.Fprintf(meta.UI.ErrorWriter, "%v\n", err) + } + + os.Exit(exitCode) +} diff --git a/const.go b/const.go new file mode 100644 index 0000000..2cf9aa6 --- /dev/null +++ b/const.go @@ -0,0 +1,23 @@ +package mailfull + +// Filenames that are contained in the Repository. +const ( + DirNameConfig = ".mailfull" + FileNameConfig = "config" + + FileNameAliasDomains = ".valiasdomains" + FileNameUsersPassword = ".vpasswd" + FileNameUserForwards = ".forward" + FileNameAliasUsers = ".valiases" + FileNameCatchAllUser = ".vcatchall" + + FileNameDbDomains = "domains" + FileNameDbDestinations = "destinations" + FileNameDbMaildirs = "maildirs" + FileNameDbLocaltable = "localtable" + FileNameDbForwards = "forwards" + FileNameDbPasswords = "vpasswd" +) + +// NeverMatchHashedPassword is hash string that is never match with any password. +const NeverMatchHashedPassword = "{SSHA}!!" diff --git a/database.go b/database.go new file mode 100644 index 0000000..6cc5ad8 --- /dev/null +++ b/database.go @@ -0,0 +1,245 @@ +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 + } + if err := dbDomains.Chown(r.uid, r.gid); err != nil { + return err + } + defer dbDomains.Close() + + for _, domain := range md.Domains { + if _, err := fmt.Fprintf(dbDomains, "%s virtual\n", domain.Name()); err != nil { + return err + } + } + + for _, aliasDomain := range md.AliasDomains { + if _, err := fmt.Fprintf(dbDomains, "%s virtual\n", aliasDomain.Name()); err != nil { + return err + } + } + + return nil +} + +func (r *Repository) generateDbDestinations(md *MailData) error { + dbDestinations, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbDestinations)) + 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 { + // 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 { + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s|%s\n", userName, domain.Name(), underscoredDomainName, user.Name()); err != nil { + return err + } + } else { + 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() { + 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 { + 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() { + if _, err := fmt.Fprintf(dbDestinations, "%s@%s %s@%s\n", aliasUser.Name(), aliasDomain.Name(), aliasUser.Name(), domain.Name()); err != nil { + return err + } + } + } + } + } + + return nil +} + +func (r *Repository) generateDbMaildirs(md *MailData) error { + dbMaildirs, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbMaildirs)) + 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 { + for _, user := range domain.Users { + if _, err := fmt.Fprintf(dbMaildirs, "%s@%s %s/%s/Maildir/\n", user.Name(), domain.Name(), domain.Name(), user.Name()); err != nil { + return err + } + } + } + + return nil +} + +func (r *Repository) generateDbLocaltable(md *MailData) error { + dbLocaltable, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbLocaltable)) + 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 { + // ho-ge.example.com -> ho_ge\.example\.com + escapedDomainName := domain.Name() + escapedDomainName = strings.Replace(escapedDomainName, `-`, `_`, -1) + escapedDomainName = strings.Replace(escapedDomainName, `.`, `\.`, -1) + + if _, err := fmt.Fprintf(dbLocaltable, "/^%s\\|.*$/ local\n", escapedDomainName); err != nil { + return err + } + } + + return nil +} + +func (r *Repository) generateDbForwards(md *MailData) error { + dbForwards, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbForwards)) + 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 { + // 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 { + if _, err := fmt.Fprintf(dbForwards, "%s|%s:%s\n", underscoredDomainName, user.Name(), strings.Join(user.Forwards(), ",")); err != nil { + return err + } + } else { + if _, err := fmt.Fprintf(dbForwards, "%s|%s:/dev/null\n", underscoredDomainName, user.Name()); err != nil { + return err + } + } + } + } + + // drop real user + if _, err := fmt.Fprintf(dbForwards, "%s:/dev/null\n", r.Username); err != nil { + return err + } + + return nil +} + +func (r *Repository) generateDbPasswords(md *MailData) error { + dbPasswords, err := os.Create(filepath.Join(r.DirDatabasePath, FileNameDbPasswords)) + 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 { + for _, user := range domain.Users { + if _, err := fmt.Fprintf(dbPasswords, "%s@%s:%s\n", user.Name(), domain.Name(), user.HashedPassword()); err != nil { + return err + } + } + } + + return nil +} diff --git a/domain.go b/domain.go new file mode 100644 index 0000000..af499e0 --- /dev/null +++ b/domain.go @@ -0,0 +1,184 @@ +package mailfull + +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" + "time" +) + +// Domain represents a Domain. +type Domain struct { + name string + Users []*User + AliasUsers []*AliasUser + 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) { + return nil, ErrInvalidDomainName + } + + d := &Domain{ + name: name, + } + + return d, nil +} + +// Name returns name. +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 +} + +// 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, 0700); err != nil { + return err + } + if err := os.Chown(domainDirPath, r.uid, r.gid); err != nil { + return err + } + + 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.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.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 +} + +// 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/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 =