1
0
mirror of https://github.com/directorz/mailfull-go.git synced 2025-12-17 09:37:02 +00:00

Merge pull request #10 from directorz/feature/implements

Feature/implements
This commit is contained in:
teru
2016-08-15 14:50:58 +09:00
committed by GitHub
38 changed files with 3777 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cli/mailfull/mailfull

View File

@@ -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!

202
aliasdomain.go Normal file
View File

@@ -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
}

245
aliasuser.go Normal file
View File

@@ -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
}

110
catchalluser.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

137
cmd/mailfull/main.go Normal file
View File

@@ -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)
}

23
const.go Normal file
View File

@@ -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}!!"

245
database.go Normal file
View File

@@ -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
}

184
domain.go Normal file
View File

@@ -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
}

140
generateconfig.go Normal file
View File

@@ -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 = </etc/pki/dovecot/certs/dovecot.pem
ssl_key = </etc/pki/dovecot/private/dovecot.pem
#ssl_ca =
disable_plaintext_auth = yes
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}
service pop3-login {
inet_listener pop3 {
port = 110
}
inet_listener pop3s {
port = 995
ssl = yes
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
}
passdb {
driver = passwd-file
args = %s
}
userdb {
driver = static
args = uid=%d gid=%d home=%s/%%d/%%n
}
passdb {
driver = pam
}
userdb {
driver = passwd
}
`,
Version, time.Now().Format(time.RFC3339),
filepath.Join(r.DirDatabasePath, FileNameDbPasswords),
r.uid, r.gid, r.DirMailDataPath,
)
return cfg[1:]
}

47
maildata.go Normal file
View File

@@ -0,0 +1,47 @@
package mailfull
// MailData represents a MailData.
type MailData struct {
Domains []*Domain
AliasDomains []*AliasDomain
}
// MailData returns a MailData.
func (r *Repository) MailData() (*MailData, error) {
domains, err := r.Domains()
if err != nil {
return nil, err
}
aliasDomains, err := r.AliasDomains()
if err != nil {
return nil, err
}
for _, domain := range domains {
users, err := r.Users(domain.Name())
if err != nil {
return nil, err
}
domain.Users = users
aliasUsers, err := r.AliasUsers(domain.Name())
if err != nil {
return nil, err
}
domain.AliasUsers = aliasUsers
catchAllUser, err := r.CatchAllUser(domain.Name())
if err != nil {
return nil, err
}
domain.CatchAllUser = catchAllUser
}
mailData := &MailData{
Domains: domains,
AliasDomains: aliasDomains,
}
return mailData, nil
}

4
mailfull.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package mailfull contains operations for a mailfull repository.
*/
package mailfull

290
repository.go Normal file
View File

@@ -0,0 +1,290 @@
package mailfull
import (
"errors"
"os"
"os/user"
"path/filepath"
"strconv"
"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")
)
// Errors for the operation of the Repository.
var (
ErrDomainNotExist = errors.New("Domain: not exist")
ErrDomainAlreadyExist = errors.New("Domain: already exist")
ErrDomainIsAliasDomainTarget = errors.New("Domain: is set as alias")
ErrAliasDomainNotExist = errors.New("AliasDomain: not exist")
ErrAliasDomainAlreadyExist = errors.New("AliasDomain: already exist")
ErrUserNotExist = errors.New("User: not exist")
ErrUserAlreadyExist = errors.New("User: already exist")
ErrUserIsCatchAllUser = errors.New("User: is set as catchall")
ErrAliasUserNotExist = errors.New("AliasUser: not exist")
ErrAliasUserAlreadyExist = errors.New("AliasUser: already exist")
ErrInvalidFormatUsersPassword = errors.New("User: password file invalid format")
ErrInvalidFormatAliasDomain = errors.New("AliasDomain: file invalid format")
ErrInvalidFormatAliasUsers = errors.New("AliasUsers: file invalid format")
)
// RepositoryConfig is used to configure a Repository.
type RepositoryConfig struct {
DirDatabasePath string `toml:"dir_database"`
DirMailDataPath string `toml:"dir_maildata"`
Username string `toml:"username"`
CmdPostalias string `toml:"postalias"`
CmdPostmap string `toml:"postmap"`
}
// Normalize normalizes paramaters of the RepositoryConfig.
func (c *RepositoryConfig) Normalize(rootPath string) {
if !filepath.IsAbs(c.DirDatabasePath) {
c.DirDatabasePath = filepath.Join(rootPath, c.DirDatabasePath)
}
if !filepath.IsAbs(c.DirMailDataPath) {
c.DirMailDataPath = filepath.Join(rootPath, c.DirMailDataPath)
}
if filepath.Base(c.CmdPostalias) != c.CmdPostalias {
if !filepath.IsAbs(c.CmdPostalias) {
c.CmdPostalias = filepath.Join(rootPath, c.CmdPostalias)
}
}
if filepath.Base(c.CmdPostmap) != c.CmdPostmap {
if !filepath.IsAbs(c.CmdPostmap) {
c.CmdPostmap = filepath.Join(rootPath, c.CmdPostmap)
}
}
}
// DefaultRepositoryConfig returns a RepositoryConfig with default parameter.
func DefaultRepositoryConfig() *RepositoryConfig {
c := &RepositoryConfig{
DirDatabasePath: "./etc",
DirMailDataPath: "./domains",
Username: "",
CmdPostalias: "postalias",
CmdPostmap: "postmap",
}
return c
}
// Repository represents a Repository.
type Repository struct {
*RepositoryConfig
uid int
gid int
}
// NewRepository creates a new Repository instance.
func NewRepository(c *RepositoryConfig) (*Repository, error) {
u, err := user.Lookup(c.Username)
if err != nil {
return nil, err
}
uid, err := strconv.Atoi(u.Uid)
if err != nil {
return nil, err
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return nil, err
}
r := &Repository{
RepositoryConfig: c,
uid: uid,
gid: gid,
}
return r, nil
}
// 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
}
c.Normalize(rootPath)
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()
u, err := user.Current()
if err != nil {
return nil
}
c := DefaultRepositoryConfig()
c.Username = u.Username
enc := toml.NewEncoder(configFile)
if err := enc.Encode(c); err != nil {
return err
}
c.Normalize(rootPath)
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, 0700); err != nil {
return err
}
} else {
return err
}
} else {
if !fi.IsDir() {
return ErrInvalidRepository
}
}
aliasDomainsFileName := filepath.Join(c.DirMailDataPath, FileNameAliasDomains)
fi, err = os.Stat(aliasDomainsFileName)
if err != nil {
if err.(*os.PathError).Err != syscall.ENOENT {
return err
}
} else {
if fi.IsDir() {
return ErrInvalidRepository
}
}
aliasDomainsFile, err := os.OpenFile(aliasDomainsFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return nil
}
defer aliasDomainsFile.Close()
return nil
}

405
user.go Normal file
View File

@@ -0,0 +1,405 @@
package mailfull
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
)
// User represents a User.
type User struct {
name string
hashedPassword string
forwards []string
}
// UserSlice attaches the methods of sort.Interface to []*User.
type UserSlice []*User
func (p UserSlice) Len() int { return len(p) }
func (p UserSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() }
func (p UserSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// NewUser creates a new User instance.
func NewUser(name, hashedPassword string, forwards []string) (*User, error) {
u := &User{}
if err := u.setName(name); err != nil {
return nil, err
}
u.SetHashedPassword(hashedPassword)
u.SetForwards(forwards)
return u, nil
}
// setName sets the name.
func (u *User) setName(name string) error {
if !validUserName(name) {
return ErrInvalidUserName
}
u.name = name
return nil
}
// Name returns name.
func (u *User) Name() string {
return u.name
}
// SetHashedPassword sets the hashed password.
func (u *User) SetHashedPassword(hashedPassword string) {
u.hashedPassword = hashedPassword
}
// HashedPassword returns hashedPassword.
func (u *User) HashedPassword() string {
return u.hashedPassword
}
// SetForwards sets forwards.
func (u *User) SetForwards(forwards []string) {
u.forwards = forwards
}
// Forwards returns forwards.
func (u *User) Forwards() []string {
return u.forwards
}
// Users returns a User slice.
func (r *Repository) Users(domainName string) ([]*User, error) {
domain, err := r.Domain(domainName)
if err != nil {
return nil, err
}
if domain == nil {
return nil, ErrDomainNotExist
}
hashedPasswords, err := r.usersHashedPassword(domainName)
if err != nil {
return nil, err
}
fileInfos, err := ioutil.ReadDir(filepath.Join(r.DirMailDataPath, domainName))
if err != nil {
return nil, err
}
users := make([]*User, 0, len(fileInfos))
for _, fileInfo := range fileInfos {
if !fileInfo.IsDir() {
continue
}
name := fileInfo.Name()
forwards, err := r.userForwards(domainName, name)
if err != nil {
return nil, err
}
hashedPassword, ok := hashedPasswords[name]
if !ok {
hashedPassword = ""
}
user, err := NewUser(name, hashedPassword, forwards)
if err != nil {
continue
}
users = append(users, user)
}
return users, nil
}
// User returns a User of the input name.
func (r *Repository) User(domainName, userName string) (*User, error) {
domain, err := r.Domain(domainName)
if err != nil {
return nil, err
}
if domain == nil {
return nil, ErrDomainNotExist
}
if !validUserName(userName) {
return nil, ErrInvalidUserName
}
hashedPasswords, err := r.usersHashedPassword(domainName)
if err != nil {
return nil, err
}
fileInfo, err := os.Stat(filepath.Join(r.DirMailDataPath, domainName, userName))
if err != nil {
if err.(*os.PathError).Err == syscall.ENOENT {
return nil, nil
}
return nil, err
}
if !fileInfo.IsDir() {
return nil, nil
}
name := userName
forwards, err := r.userForwards(domainName, name)
if err != nil {
return nil, err
}
hashedPassword, ok := hashedPasswords[name]
if !ok {
hashedPassword = ""
}
user, err := NewUser(name, hashedPassword, forwards)
if err != nil {
return nil, err
}
return user, nil
}
// usersHashedPassword returns a string map of usernames to the hashed password.
func (r *Repository) usersHashedPassword(domainName string) (map[string]string, error) {
if !validDomainName(domainName) {
return nil, ErrInvalidDomainName
}
file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword))
if err != nil {
return nil, err
}
defer file.Close()
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) {
if !validDomainName(domainName) {
return nil, ErrInvalidDomainName
}
if !validUserName(userName) {
return nil, ErrInvalidUserName
}
file, err := os.Open(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards))
if err != nil {
if err.(*os.PathError).Err == syscall.ENOENT {
return nil, nil
}
return nil, err
}
defer file.Close()
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
}
// UserCreate creates the input User.
func (r *Repository) UserCreate(domainName string, user *User) error {
existUser, err := r.User(domainName, user.Name())
if err != nil {
return err
}
if existUser != nil {
return ErrUserAlreadyExist
}
existAliasUser, err := r.AliasUser(domainName, user.Name())
if err != nil {
return err
}
if existAliasUser != nil {
return ErrAliasUserAlreadyExist
}
userDirPath := filepath.Join(r.DirMailDataPath, domainName, user.Name())
dirNames := []string{
userDirPath,
filepath.Join(userDirPath, "Maildir"),
filepath.Join(userDirPath, "Maildir/cur"),
filepath.Join(userDirPath, "Maildir/new"),
filepath.Join(userDirPath, "Maildir/tmp"),
}
for _, dirName := range dirNames {
if err := os.Mkdir(dirName, 0700); err != nil {
return err
}
if err := os.Chown(dirName, r.uid, r.gid); err != nil {
return err
}
}
if err := r.UserUpdate(domainName, user); err != nil {
return err
}
return nil
}
// UserUpdate updates the input User.
func (r *Repository) UserUpdate(domainName string, user *User) error {
existUser, err := r.User(domainName, user.Name())
if err != nil {
return err
}
if existUser == nil {
return ErrUserNotExist
}
hashedPasswords, err := r.usersHashedPassword(domainName)
if err != nil {
return err
}
hashedPasswords[user.Name()] = user.HashedPassword()
if err := r.writeUsersPasswordFile(domainName, hashedPasswords); err != nil {
return err
}
if err := r.writeUserForwardsFile(domainName, user.Name(), user.Forwards()); err != nil {
return err
}
return nil
}
// UserRemove removes a User of the input name.
func (r *Repository) UserRemove(domainName, userName string) error {
existUser, err := r.User(domainName, userName)
if err != nil {
return err
}
if existUser == nil {
return ErrUserNotExist
}
catchAllUser, err := r.CatchAllUser(domainName)
if err != nil {
return err
}
if catchAllUser != nil && catchAllUser.Name() == userName {
return ErrUserIsCatchAllUser
}
hashedPasswords, err := r.usersHashedPassword(domainName)
if err != nil {
return err
}
delete(hashedPasswords, userName)
if err := r.writeUsersPasswordFile(domainName, hashedPasswords); err != nil {
return err
}
userDirPath := filepath.Join(r.DirMailDataPath, domainName, userName)
userBackupDirPath := filepath.Join(r.DirMailDataPath, domainName, "."+userName+".deleted."+time.Now().Format("20060102150405"))
if err := os.Rename(userDirPath, userBackupDirPath); err != nil {
return err
}
return nil
}
// writeUsersPasswordFile writes passwords of each users to the file.
func (r *Repository) writeUsersPasswordFile(domainName string, hashedPasswords map[string]string) error {
if !validDomainName(domainName) {
return ErrInvalidDomainName
}
keys := make([]string, 0, len(hashedPasswords))
for key := range hashedPasswords {
keys = append(keys, key)
}
sort.Strings(keys)
file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, FileNameUsersPassword), os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer file.Close()
for _, key := range keys {
if _, err := fmt.Fprintf(file, "%s:%s\n", key, hashedPasswords[key]); err != nil {
return err
}
}
return nil
}
// writeUserForwardsFile writes forwards to user's forward file.
func (r *Repository) writeUserForwardsFile(domainName, userName string, forwards []string) error {
if !validDomainName(domainName) {
return ErrInvalidDomainName
}
if !validUserName(userName) {
return ErrInvalidUserName
}
file, err := os.OpenFile(filepath.Join(r.DirMailDataPath, domainName, userName, FileNameUserForwards), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
if err := file.Chown(r.uid, r.gid); err != nil {
return err
}
defer file.Close()
for _, forward := range forwards {
if _, err := fmt.Fprintf(file, "%s\n", forward); err != nil {
return err
}
}
return nil
}

52
valid.go Normal file
View File

@@ -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-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-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-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-z]+$`).MatchString(target)
}
// validCatchAllUserName returns true if the input is correct format.
func validCatchAllUserName(name string) bool {
return regexp.MustCompile(`^[^\s@]+$`).MatchString(name)
}

4
version.go Normal file
View File

@@ -0,0 +1,4 @@
package mailfull
// Version is a version number.
const Version = "0.0.1"