diff --git a/Makefile b/Makefile index f3e73c9..a1dc559 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,17 @@ installdeps: gom install build: - go build -ldflags "-X main.gittag=`git rev-parse --short HEAD`" -v -o build/mailfull_$(GOOS)_$(GOARCH)/mailfull cmd/mailfull/main.go + go build -v -ldflags "-X main.gittag=`git rev-parse --short HEAD`" -o build/mailfull_$(GOOS)_$(GOARCH)/mailfull cmd/mailfull/mailfull.go build-linux-amd64: docker run --rm -v $(PWD):/go/src/github.com/directorz/mailfull-go -w /go/src/github.com/directorz/mailfull-go \ -e GOOS=linux -e GOARCH=amd64 golang:1.7 \ - go build -ldflags "-X main.gittag=`git rev-parse --short HEAD`" -v -o "build/mailfull_linux_amd64/mailfull" cmd/mailfull/main.go + go build -v -ldflags "-X main.gittag=`git rev-parse --short HEAD`" -o "build/mailfull_linux_amd64/mailfull" cmd/mailfull/mailfull.go build-linux-386: docker run --rm -v $(PWD):/go/src/github.com/directorz/mailfull-go -w /go/src/github.com/directorz/mailfull-go \ -e GOOS=linux -e GOARCH=386 golang:1.7 \ - go build -ldflags "-X main.gittag=`git rev-parse --short HEAD`" -v -o "build/mailfull_linux_386/mailfull" cmd/mailfull/main.go + go build -v -ldflags "-X main.gittag=`git rev-parse --short HEAD`" -o "build/mailfull_linux_386/mailfull" cmd/mailfull/mailfull.go release: release-linux-amd64 release-linux-386 diff --git a/README.md b/README.md index 9ead86e..d2857fc 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ Features Installation ------------ +### Binary + +You can download archive file from [releases page](https://github.com/directorz/mailfull-go/releases) . +Download and unpack the archive file, and put the binary file to somewhere you want. + ### go get Installed in `$GOPATH/bin` diff --git a/cmd/mailfull/command/domaindisable.go b/cmd/mailfull/command/domaindisable.go new file mode 100644 index 0000000..98e69f6 --- /dev/null +++ b/cmd/mailfull/command/domaindisable.go @@ -0,0 +1,83 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// DomainDisableCommand represents a DomainDisableCommand. +type DomainDisableCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *DomainDisableCommand) Synopsis() string { + return "Disable a domain temporarily." +} + +// Help returns long-form help text. +func (c *DomainDisableCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name that you want to disable. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *DomainDisableCommand) 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 := repo.Domain(domainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + if domain == nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", mailfull.ErrDomainNotExist) + return 1 + } + + domain.SetDisabled(true) + + if err := repo.DomainUpdate(domain); 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/domainenable.go b/cmd/mailfull/command/domainenable.go new file mode 100644 index 0000000..25e782d --- /dev/null +++ b/cmd/mailfull/command/domainenable.go @@ -0,0 +1,83 @@ +package command + +import ( + "fmt" + + "github.com/directorz/mailfull-go" +) + +// DomainEnableCommand represents a DomainEnableCommand. +type DomainEnableCommand struct { + Meta +} + +// Synopsis returns a one-line synopsis. +func (c *DomainEnableCommand) Synopsis() string { + return "Enable a domain." +} + +// Help returns long-form help text. +func (c *DomainEnableCommand) Help() string { + txt := fmt.Sprintf(` +Usage: + %s %s domain + +Description: + %s + +Required Args: + domain + The domain name that you want to enable. +`, + c.CmdName, c.SubCmdName, + c.Synopsis()) + + return txt[1:] +} + +// Run runs the command and returns the exit status. +func (c *DomainEnableCommand) 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 := repo.Domain(domainName) + if err != nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", err) + return 1 + } + if domain == nil { + fmt.Fprintf(c.UI.ErrorWriter, "[ERR] %v\n", mailfull.ErrDomainNotExist) + return 1 + } + + domain.SetDisabled(false) + + if err := repo.DomainUpdate(domain); 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 index 3d7f197..83b65d5 100644 --- a/cmd/mailfull/command/domains.go +++ b/cmd/mailfull/command/domains.go @@ -25,6 +25,7 @@ Usage: Description: %s + Disabled domains are marked "!" the beginning. `, c.CmdName, c.SubCmdName, c.Synopsis()) @@ -48,7 +49,12 @@ func (c *DomainsCommand) Run(args []string) int { sort.Sort(mailfull.DomainSlice(domains)) for _, domain := range domains { - fmt.Fprintf(c.UI.Writer, "%s\n", domain.Name()) + disableStr := "" + if domain.Disabled() { + disableStr = "!" + } + + fmt.Fprintf(c.UI.Writer, "%s%s\n", disableStr, domain.Name()) } return 0 diff --git a/cmd/mailfull/main.go b/cmd/mailfull/mailfull.go similarity index 92% rename from cmd/mailfull/main.go rename to cmd/mailfull/mailfull.go index 83154d7..905a77a 100644 --- a/cmd/mailfull/main.go +++ b/cmd/mailfull/mailfull.go @@ -62,6 +62,14 @@ func main() { meta.SubCmdName = c.Subcommand() return &command.DomainDelCommand{Meta: meta}, nil }, + "domaindisable": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.DomainDisableCommand{Meta: meta}, nil + }, + "domainenable": func() (cli.Command, error) { + meta.SubCmdName = c.Subcommand() + return &command.DomainEnableCommand{Meta: meta}, nil + }, "aliasdomains": func() (cli.Command, error) { meta.SubCmdName = c.Subcommand() return &command.AliasDomainsCommand{Meta: meta}, nil diff --git a/const.go b/const.go index 2cf9aa6..b768edd 100644 --- a/const.go +++ b/const.go @@ -5,6 +5,7 @@ const ( DirNameConfig = ".mailfull" FileNameConfig = "config" + FileNameDomainDisable = ".vdomaindisable" FileNameAliasDomains = ".valiasdomains" FileNameUsersPassword = ".vpasswd" FileNameUserForwards = ".forward" diff --git a/database.go b/database.go index 6cc5ad8..e12cf5c 100644 --- a/database.go +++ b/database.go @@ -70,6 +70,10 @@ func (r *Repository) generateDbDomains(md *MailData) error { defer dbDomains.Close() for _, domain := range md.Domains { + if domain.Disabled() { + continue + } + if _, err := fmt.Fprintf(dbDomains, "%s virtual\n", domain.Name()); err != nil { return err } @@ -95,6 +99,10 @@ func (r *Repository) generateDbDestinations(md *MailData) error { defer dbDestinations.Close() for _, domain := range md.Domains { + if domain.Disabled() { + continue + } + // ho-ge.example.com -> ho_ge.example.com underscoredDomainName := domain.Name() underscoredDomainName = strings.Replace(underscoredDomainName, `-`, `_`, -1) @@ -153,6 +161,10 @@ func (r *Repository) generateDbMaildirs(md *MailData) error { defer dbMaildirs.Close() for _, domain := range md.Domains { + if domain.Disabled() { + continue + } + 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 @@ -174,6 +186,10 @@ func (r *Repository) generateDbLocaltable(md *MailData) error { defer dbLocaltable.Close() for _, domain := range md.Domains { + if domain.Disabled() { + continue + } + // ho-ge.example.com -> ho_ge\.example\.com escapedDomainName := domain.Name() escapedDomainName = strings.Replace(escapedDomainName, `-`, `_`, -1) @@ -198,6 +214,10 @@ func (r *Repository) generateDbForwards(md *MailData) error { defer dbForwards.Close() for _, domain := range md.Domains { + if domain.Disabled() { + continue + } + // ho-ge.example.com -> ho_ge.example.com underscoredDomainName := domain.Name() underscoredDomainName = strings.Replace(underscoredDomainName, `-`, `_`, -1) @@ -234,6 +254,10 @@ func (r *Repository) generateDbPasswords(md *MailData) error { defer dbPasswords.Close() for _, domain := range md.Domains { + if domain.Disabled() { + continue + } + for _, user := range domain.Users { if _, err := fmt.Fprintf(dbPasswords, "%s@%s:%s\n", user.Name(), domain.Name(), user.HashedPassword()); err != nil { return err diff --git a/domain.go b/domain.go index af499e0..c0089d7 100644 --- a/domain.go +++ b/domain.go @@ -11,6 +11,7 @@ import ( // Domain represents a Domain. type Domain struct { name string + disabled bool Users []*User AliasUsers []*AliasUser CatchAllUser *CatchAllUser @@ -25,22 +26,41 @@ 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{} - d := &Domain{ - name: name, + if err := d.setName(name); err != nil { + return nil, err } return d, nil } +// setName sets the name. +func (d *Domain) setName(name string) error { + if !validDomainName(name) { + return ErrInvalidDomainName + } + + d.name = name + + return nil +} + // Name returns name. func (d *Domain) Name() string { return d.name } +// SetDisabled disables the Domain if the input is true. +func (d *Domain) SetDisabled(disabled bool) { + d.disabled = disabled +} + +// Disabled returns true if the Domain is disabled. +func (d *Domain) Disabled() bool { + return d.disabled +} + // Domains returns a Domain slice. func (r *Repository) Domains() ([]*Domain, error) { fileInfos, err := ioutil.ReadDir(r.DirMailDataPath) @@ -62,6 +82,12 @@ func (r *Repository) Domains() ([]*Domain, error) { continue } + disabled, err := r.domainDisabled(name) + if err != nil { + return nil, err + } + domain.SetDisabled(disabled) + domains = append(domains, domain) } @@ -94,9 +120,38 @@ func (r *Repository) Domain(domainName string) (*Domain, error) { return nil, err } + disabled, err := r.domainDisabled(name) + if err != nil { + return nil, err + } + domain.SetDisabled(disabled) + return domain, nil } +// domainDisabled returns true if the input Domain is disabled. +func (r *Repository) domainDisabled(domainName string) (bool, error) { + if !validDomainName(domainName) { + return false, ErrInvalidDomainName + } + + fi, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName, FileNameDomainDisable)) + + if err != nil { + if err.(*os.PathError).Err == syscall.ENOENT { + return false, nil + } + + return false, err + } + + if fi.IsDir() { + return false, ErrInvalidFormatDomainDisabled + } + + return true, nil +} + // DomainCreate creates the input Domain. func (r *Repository) DomainCreate(domain *Domain) error { existDomain, err := r.Domain(domain.Name()) @@ -150,6 +205,29 @@ func (r *Repository) DomainCreate(domain *Domain) error { } catchAllUserFile.Close() + if domain.Disabled() { + if err := r.writeDomainDisabledFile(domain.Name(), domain.Disabled()); err != nil { + return err + } + } + + return nil +} + +// DomainUpdate updates the input Domain. +func (r *Repository) DomainUpdate(domain *Domain) error { + existDomain, err := r.Domain(domain.Name()) + if err != nil { + return err + } + if existDomain == nil { + return ErrDomainNotExist + } + + if err := r.writeDomainDisabledFile(domain.Name(), domain.Disabled()); err != nil { + return err + } + return nil } @@ -182,3 +260,36 @@ func (r *Repository) DomainRemove(domainName string) error { return nil } + +// writeDomainDisabledFile creates/removes the disabled file. +func (r *Repository) writeDomainDisabledFile(domainName string, disabled bool) error { + if !validDomainName(domainName) { + return ErrInvalidDomainName + } + + nowDisabled, err := r.domainDisabled(domainName) + if err != nil { + return err + } + + domainDisabledFileName := filepath.Join(r.DirMailDataPath, domainName, FileNameDomainDisable) + + if !nowDisabled && disabled { + file, err := os.OpenFile(domainDisabledFileName, 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 + } + file.Close() + } + + if nowDisabled && !disabled { + if err := os.Remove(domainDisabledFileName); err != nil { + return err + } + } + + return nil +} diff --git a/repository.go b/repository.go index 187c997..f2f3717 100644 --- a/repository.go +++ b/repository.go @@ -34,9 +34,10 @@ var ( ErrAliasUserNotExist = errors.New("AliasUser: not exist") ErrAliasUserAlreadyExist = errors.New("AliasUser: already exist") - ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format") - ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format") - ErrInvalidFormatAliasUsers = errors.New("AliasUsers: file invalid format") + ErrInvalidFormatDomainDisabled = errors.New("Domain: disabled file invalid format") + 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. diff --git a/version.go b/version.go index 51658b0..ab66701 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package mailfull // Version is a version number. -const Version = "v0.0.3" +const Version = "v0.0.4"