mirror of
https://github.com/directorz/mailfull-go.git
synced 2025-12-17 17:47:04 +00:00
Merge pull request #10 from directorz/feature/implements
Feature/implements
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cli/mailfull/mailfull
|
||||
62
README.md
62
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!
|
||||
|
||||
202
aliasdomain.go
Normal file
202
aliasdomain.go
Normal 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
245
aliasuser.go
Normal 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
110
catchalluser.go
Normal 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
|
||||
}
|
||||
80
cmd/mailfull/command/aliasdomainadd.go
Normal file
80
cmd/mailfull/command/aliasdomainadd.go
Normal 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
|
||||
}
|
||||
71
cmd/mailfull/command/aliasdomaindel.go
Normal file
71
cmd/mailfull/command/aliasdomaindel.go
Normal 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
|
||||
}
|
||||
75
cmd/mailfull/command/aliasdomains.go
Normal file
75
cmd/mailfull/command/aliasdomains.go
Normal 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
|
||||
}
|
||||
89
cmd/mailfull/command/aliasuseradd.go
Normal file
89
cmd/mailfull/command/aliasuseradd.go
Normal 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
|
||||
}
|
||||
79
cmd/mailfull/command/aliasuserdel.go
Normal file
79
cmd/mailfull/command/aliasuserdel.go
Normal 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
|
||||
}
|
||||
98
cmd/mailfull/command/aliasusermod.go
Normal file
98
cmd/mailfull/command/aliasusermod.go
Normal 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
|
||||
}
|
||||
66
cmd/mailfull/command/aliasusers.go
Normal file
66
cmd/mailfull/command/aliasusers.go
Normal 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
|
||||
}
|
||||
64
cmd/mailfull/command/catchall.go
Normal file
64
cmd/mailfull/command/catchall.go
Normal 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
|
||||
}
|
||||
80
cmd/mailfull/command/catchallset.go
Normal file
80
cmd/mailfull/command/catchallset.go
Normal 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
|
||||
}
|
||||
71
cmd/mailfull/command/catchallunset.go
Normal file
71
cmd/mailfull/command/catchallunset.go
Normal 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
|
||||
}
|
||||
55
cmd/mailfull/command/commit.go
Normal file
55
cmd/mailfull/command/commit.go
Normal 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
|
||||
}
|
||||
88
cmd/mailfull/command/domainadd.go
Normal file
88
cmd/mailfull/command/domainadd.go
Normal 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
|
||||
}
|
||||
71
cmd/mailfull/command/domaindel.go
Normal file
71
cmd/mailfull/command/domaindel.go
Normal 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
|
||||
}
|
||||
55
cmd/mailfull/command/domains.go
Normal file
55
cmd/mailfull/command/domains.go
Normal 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
|
||||
}
|
||||
67
cmd/mailfull/command/genconfig.go
Normal file
67
cmd/mailfull/command/genconfig.go
Normal 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
|
||||
}
|
||||
42
cmd/mailfull/command/init.go
Normal file
42
cmd/mailfull/command/init.go
Normal 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
|
||||
}
|
||||
13
cmd/mailfull/command/main.go
Normal file
13
cmd/mailfull/command/main.go
Normal 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
|
||||
}
|
||||
86
cmd/mailfull/command/useradd.go
Normal file
86
cmd/mailfull/command/useradd.go
Normal 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
|
||||
}
|
||||
100
cmd/mailfull/command/usercheckpw.go
Normal file
100
cmd/mailfull/command/usercheckpw.go
Normal 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
|
||||
}
|
||||
80
cmd/mailfull/command/userdel.go
Normal file
80
cmd/mailfull/command/userdel.go
Normal 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
|
||||
}
|
||||
131
cmd/mailfull/command/userpasswd.go
Normal file
131
cmd/mailfull/command/userpasswd.go
Normal 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
|
||||
}
|
||||
66
cmd/mailfull/command/users.go
Normal file
66
cmd/mailfull/command/users.go
Normal 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
137
cmd/mailfull/main.go
Normal 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
23
const.go
Normal 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
245
database.go
Normal 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
184
domain.go
Normal 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
140
generateconfig.go
Normal 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
47
maildata.go
Normal 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
4
mailfull.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package mailfull contains operations for a mailfull repository.
|
||||
*/
|
||||
package mailfull
|
||||
290
repository.go
Normal file
290
repository.go
Normal 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
405
user.go
Normal 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
52
valid.go
Normal 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
4
version.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package mailfull
|
||||
|
||||
// Version is a version number.
|
||||
const Version = "0.0.1"
|
||||
Reference in New Issue
Block a user