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.
|
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