679 lines
15 KiB
Go
679 lines
15 KiB
Go
package modDovecot
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/zloylos/grsync"
|
|
"golang.org/x/crypto/ssh"
|
|
kh "golang.org/x/crypto/ssh/knownhosts"
|
|
|
|
"git.ververis.eu/mandi/modDovecot/types"
|
|
)
|
|
|
|
const serialServerPath string = "srv"
|
|
|
|
type MailServer struct {
|
|
ApiConfig *ApiConfig
|
|
SSHConfig *SSHConfig
|
|
MaildirPath string
|
|
BakupMailsPath string
|
|
BackupAccountsPath string
|
|
MailDirOwner string
|
|
MailDirGroup string
|
|
}
|
|
|
|
type server struct {
|
|
server *MailServer
|
|
sshClientConfig *ssh.ClientConfig
|
|
apiKey *ApiKey
|
|
sshAddress string
|
|
}
|
|
|
|
type SSHConfig struct {
|
|
SSHHost string
|
|
SSHPort string
|
|
SSHUser string
|
|
LocalPrivateKeyPath string
|
|
LocalKnownHostPath string
|
|
}
|
|
|
|
type ApiConfig struct {
|
|
ApiHost string
|
|
ApiUser string
|
|
Password string
|
|
}
|
|
|
|
//Begin types
|
|
type ApiKey struct {
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
type IPacket interface {
|
|
getPackets() ([]*types.Packet, error)
|
|
}
|
|
|
|
type PacketValues struct {
|
|
Packets []*types.Packet
|
|
}
|
|
|
|
type FullPacket struct {
|
|
Packets []*types.Packet
|
|
}
|
|
|
|
type FilePacket struct {
|
|
FilePath string
|
|
Packets []*types.Packet
|
|
}
|
|
|
|
type AntivirType string
|
|
|
|
const (
|
|
Off AntivirType = "off"
|
|
In AntivirType = "in"
|
|
Out AntivirType = "out"
|
|
Inout AntivirType = "inout"
|
|
)
|
|
|
|
type PasswordType string
|
|
|
|
const (
|
|
Plain PasswordType = "plain"
|
|
Crypt PasswordType = "crypt"
|
|
)
|
|
|
|
type ApiAction string
|
|
|
|
const (
|
|
Create ApiAction = "create"
|
|
)
|
|
|
|
//Test
|
|
func (s *server) TestSSH() error {
|
|
|
|
if s.sshClientConfig == nil {
|
|
return errors.New("incorrect ssh configuration")
|
|
}
|
|
|
|
conn, err := ssh.Dial("tcp", s.sshAddress, s.sshClientConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
return err
|
|
|
|
}
|
|
defer session.Close()
|
|
|
|
out, err := session.CombinedOutput("pwd")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Test SSH gave back: ", string(out))
|
|
return nil
|
|
}
|
|
|
|
//End Test
|
|
|
|
//End types
|
|
//Begin IPacket interface
|
|
func (p *PacketValues) AddPacket(siteId int, action ApiAction, name, password string, enabled bool, passwordType PasswordType, antivirType AntivirType) {
|
|
np := createPacket(siteId, action, name, password, enabled, passwordType, antivirType)
|
|
p.Packets = append(p.Packets, &np)
|
|
}
|
|
|
|
func (p PacketValues) getPackets() ([]*types.Packet, error) {
|
|
return p.Packets, nil
|
|
}
|
|
|
|
func (p FullPacket) getPackets() ([]*types.Packet, error) {
|
|
return p.Packets, nil
|
|
}
|
|
|
|
func (p FilePacket) getPackets() ([]*types.Packet, error) {
|
|
file, err := ioutil.ReadFile(p.FilePath)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
accounts := []types.Account{}
|
|
_ = json.Unmarshal([]byte(file), &accounts)
|
|
for _, v := range accounts {
|
|
name := strings.Split(v.Address, "@")[0]
|
|
packet := createPacket(1, Create, name, v.Password, true, Plain, Inout)
|
|
p.Packets = append(p.Packets, &packet)
|
|
}
|
|
|
|
return p.Packets, nil
|
|
}
|
|
|
|
//End IPacket interface
|
|
|
|
//Constructor
|
|
func (ms *MailServer) NewServer() *server {
|
|
//Make sure to generate server serialisation on program start
|
|
srv := server{
|
|
server: ms,
|
|
}
|
|
|
|
sshClientConfig, err := srv.getSSHConfig()
|
|
if err != nil {
|
|
fmt.Println("Constructor: ", err)
|
|
}
|
|
srv.sshClientConfig = sshClientConfig
|
|
srv.sshAddress = srv.server.SSHConfig.SSHHost + ":" + srv.server.SSHConfig.SSHPort
|
|
|
|
srv.logIn()
|
|
return &srv
|
|
}
|
|
|
|
//Begin server implementation
|
|
//Stores Backup of mail directory in given folder using rsync
|
|
func (s *server) SynchroniseMail(delete bool) (bool, error) {
|
|
source := s.server.SSHConfig.SSHUser + "@" + s.server.SSHConfig.SSHHost + ":" + s.server.MaildirPath
|
|
fmt.Println(source)
|
|
task := grsync.NewTask(
|
|
source,
|
|
s.server.BakupMailsPath,
|
|
grsync.RsyncOptions{
|
|
Delete: delete,
|
|
},
|
|
)
|
|
|
|
go func() {
|
|
for {
|
|
state := task.State()
|
|
fmt.Printf(
|
|
"progress: %.2f / rem. %d / tot. %d / sp. %s \n",
|
|
state.Progress,
|
|
state.Remain,
|
|
state.Total,
|
|
state.Speed,
|
|
)
|
|
<-time.After(time.Second)
|
|
return
|
|
}
|
|
}()
|
|
|
|
if err := task.Run(); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
fmt.Println("well done")
|
|
fmt.Println("###LOG###")
|
|
fmt.Println(task.Log())
|
|
fmt.Println("###END LOG###")
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func BackupMail() {
|
|
|
|
}
|
|
|
|
//Stores Backup of accounts
|
|
func (s *server) BackupAccounts() (bool, error) {
|
|
|
|
accountsList, err := s.createAccountBackup()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
r := csv.NewReader(strings.NewReader(accountsList))
|
|
r.Comma = '|'
|
|
r.Comment = '+'
|
|
r.FieldsPerRecord = -1
|
|
|
|
records, err := r.ReadAll()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
accounts := []types.Account{}
|
|
|
|
for _, v := range records {
|
|
if len(v) == 5 {
|
|
address := strings.Replace(v[1], " ", "", -1)
|
|
password := strings.Replace(v[3], " ", "", -1)
|
|
if address != "address" {
|
|
|
|
accounts = append(accounts, types.Account{
|
|
Address: address,
|
|
Password: password,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(s.server.BackupAccountsPath); errors.Is(err, os.ErrNotExist) {
|
|
err := os.MkdirAll(s.server.BackupAccountsPath, os.ModePerm)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
file, _ := json.MarshalIndent(accounts, "", " ")
|
|
err = ioutil.WriteFile(s.server.BackupAccountsPath+"/"+backupName(), file, 0644)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *server) RestoreAccounts(a ...IPacket) {
|
|
for _, v := range a {
|
|
packets, err := v.getPackets()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
for _, p := range packets {
|
|
|
|
xml, err := CreateXMLString(*p)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
res, err := s.CreateAccount(xml)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
fmt.Println(res.Mail.Action.Result.Status)
|
|
fmt.Println(res.Mail.Action.Result.ErrorText)
|
|
}
|
|
}
|
|
}
|
|
|
|
//RestoreMail restores the Maildir from backup directory to the source directory specified on Server struct
|
|
func (s *server) RestoreMail() (bool, error) {
|
|
dest := s.server.SSHConfig.SSHUser + "@" + s.server.SSHConfig.SSHHost + ":" + s.server.MaildirPath
|
|
//fmt.Println(source)
|
|
//s.SSHUser+"@"+s.SSHHost+":/root/sync/",
|
|
task := grsync.NewTask(
|
|
"sync/ververis.eu/test",
|
|
dest,
|
|
grsync.RsyncOptions{
|
|
Delete: true,
|
|
},
|
|
)
|
|
|
|
go func() {
|
|
for {
|
|
state := task.State()
|
|
fmt.Printf(
|
|
"progress: %.2f / rem. %d / tot. %d / sp. %s \n",
|
|
state.Progress,
|
|
state.Remain,
|
|
state.Total,
|
|
state.Speed,
|
|
)
|
|
<-time.After(time.Second)
|
|
return
|
|
}
|
|
}()
|
|
|
|
if err := task.Run(); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
fmt.Println("well done")
|
|
fmt.Println("###LOG###")
|
|
fmt.Println(task.Log())
|
|
fmt.Println("###END LOG###")
|
|
|
|
out, err := s.changeOwnership()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
fmt.Println(out)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func CreateXMLString(p types.Packet) (string, error) {
|
|
|
|
if xmlstring, err := xml.MarshalIndent(p, "", " "); err == nil {
|
|
xmlstring = []byte(xml.Header + string(xmlstring))
|
|
return string(xmlstring), nil
|
|
} else {
|
|
return "", err
|
|
}
|
|
|
|
}
|
|
|
|
//Begin API (plesk specific) functions
|
|
//Create a single account passing the payload as xml string
|
|
func (s *server) CreateAccount(payloadXML string) (types.Packet, error) {
|
|
r := &types.Packet{}
|
|
if (ApiKey{}) == *s.apiKey {
|
|
err := s.logIn()
|
|
if err != nil {
|
|
return *r, err
|
|
}
|
|
}
|
|
|
|
url := s.server.ApiConfig.ApiHost + "/enterprise/control/agent.php"
|
|
method := "GET"
|
|
|
|
payload := strings.NewReader(payloadXML)
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequest(method, url, payload)
|
|
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return *r, err
|
|
}
|
|
req.Header.Add("KEY", s.apiKey.Key)
|
|
req.Header.Add("Content-Type", "application/xml")
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return *r, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return *r, err
|
|
}
|
|
|
|
response := string(body)
|
|
|
|
err = xml.Unmarshal([]byte(response), r)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
//fmt.Println(response)
|
|
|
|
return *r, nil
|
|
}
|
|
|
|
func (s *server) logIn() error {
|
|
serializedServer := server{}
|
|
apiKey := ApiKey{}
|
|
//Check file existance
|
|
if _, err := os.Stat(serialServerPath); err == nil {
|
|
//Load serialized server to check if an API-Key is stored
|
|
file, err := ioutil.ReadFile(serialServerPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(file), &serializedServer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//If an API-Key exists store set to apiKey variable
|
|
if (ApiKey{}) != *serializedServer.apiKey {
|
|
apiKey = *serializedServer.apiKey
|
|
} else {
|
|
apiKey, err = s.callApiKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
} else if errors.Is(err, os.ErrNotExist) {
|
|
apiKey, err = s.callApiKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
s.apiKey = &apiKey
|
|
fmt.Println("The Api key is: " + s.apiKey.Key)
|
|
//fmt.Println(s)
|
|
|
|
file, _ := json.MarshalIndent(s.apiKey, "", " ")
|
|
err := ioutil.WriteFile(serialServerPath, file, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//Begin Internal supporting functions
|
|
|
|
func (s *server) MarshalJSON() ([]byte, error) {
|
|
marshal, err := json.MarshalIndent(struct {
|
|
ApiKey *ApiKey
|
|
}{
|
|
ApiKey: s.apiKey,
|
|
}, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshal, nil
|
|
}
|
|
|
|
func (s *server) UnmarshalJSON(b []byte) error {
|
|
return json.Unmarshal(b, &s.apiKey)
|
|
}
|
|
|
|
func (s *server) getSSHConfig() (*ssh.ClientConfig, error) {
|
|
key, err := ioutil.ReadFile(s.server.SSHConfig.LocalPrivateKeyPath)
|
|
if err != nil {
|
|
fmt.Println("unable to read private key: ", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Create the Signer for this private key.
|
|
signer, err := ssh.ParsePrivateKey(key)
|
|
if err != nil {
|
|
fmt.Println("unable to parse private key: ", err)
|
|
return nil, err
|
|
}
|
|
|
|
hostKeyCallback, err := kh.New(s.server.SSHConfig.LocalKnownHostPath)
|
|
if err != nil {
|
|
fmt.Println("could not create hostkeycallback function: ", err)
|
|
return nil, err
|
|
}
|
|
|
|
config := &ssh.ClientConfig{
|
|
User: s.server.SSHConfig.SSHUser,
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.PublicKeys(signer),
|
|
},
|
|
HostKeyCallback: hostKeyCallback,
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *server) callApiKey() (ApiKey, error) {
|
|
apiKey := ApiKey{}
|
|
var auth string
|
|
//Create basicAuth using user and password
|
|
if s.server.ApiConfig.ApiUser != "" && s.server.ApiConfig.Password != "" {
|
|
auth = basicAuth(s.server.ApiConfig.ApiUser, s.server.ApiConfig.Password)
|
|
} else {
|
|
return apiKey, errors.New("credential missing")
|
|
}
|
|
|
|
url := s.server.ApiConfig.ApiHost + "/api/v2/auth/keys"
|
|
method := "POST"
|
|
|
|
payload := strings.NewReader(`{}`)
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequest(method, url, payload)
|
|
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return apiKey, err
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
req.Header.Add("Accept", "application/json")
|
|
req.Header.Add("Authorization", "Basic "+auth)
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return apiKey, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return apiKey, err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(body), &apiKey)
|
|
if err != nil {
|
|
return apiKey, err
|
|
}
|
|
return apiKey, nil
|
|
}
|
|
|
|
//Connects to server over ssh, performs a qurey of all existing accounts and returns a csv string
|
|
func (s *server) createAccountBackup() (string, error) {
|
|
|
|
// A public key may be used to authenticate against the remote
|
|
// server by using an unencrypted PEM-encoded private key file.
|
|
//
|
|
// If you have an encrypted private key, the crypto/x509 package
|
|
// can be used to decrypt it.
|
|
/*
|
|
var hostKey ssh.PublicKey
|
|
key, err := ioutil.ReadFile("/Users/mandi/.ssh/id_rsa")
|
|
if err != nil {
|
|
log.Fatalf("unable to read private key: %v", err)
|
|
}
|
|
|
|
// Create the Signer for this private key.
|
|
|
|
signer, err := ssh.ParsePrivateKey(key)
|
|
if err != nil {
|
|
log.Fatalf("unable to parse private key: %v", err)
|
|
}
|
|
*/
|
|
|
|
config, err := s.getSSHConfig()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
conn, err := ssh.Dial("tcp", s.sshAddress, config)
|
|
if err != nil {
|
|
log.Fatal("Failed to dial: ", err)
|
|
return "", err
|
|
}
|
|
defer conn.Close()
|
|
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
fmt.Println(err.Error())
|
|
return "", err
|
|
}
|
|
defer session.Close()
|
|
|
|
out, err := session.CombinedOutput("/usr/local/psa/admin/sbin/mail_auth_view;")
|
|
if err != nil {
|
|
fmt.Println("remote execution CMD failed", err)
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
//Creates a name for the backup, based on date and time
|
|
func backupName() string {
|
|
return string(time.Now().Format("2006-01-02-15:04:05")) + ".json"
|
|
}
|
|
|
|
//Creates the needed xml string for api payload getting an instance of types.Packet struct
|
|
|
|
/*
|
|
Created Packet struct getting the needed parameters
|
|
Parameters:
|
|
action #string: Use "create"
|
|
siteId #int: The ID of the plesk customer account default should be 1
|
|
name #string: The email name without the @domain.tld
|
|
enabled #bool: Marks the mail address as active default true
|
|
passsword #string: The plain text password of the mail account
|
|
passwordType #string: The type of the password. Default "plain"
|
|
antivirType #string: The type of antivirus behaviour. Default inout
|
|
Options:
|
|
# inout ativirus for incoming and outgoing mails
|
|
# in only incoming mails are checked
|
|
# out only outgoing mails are checked
|
|
# off antivirus turned off for incoming and outgoing mails
|
|
See for details
|
|
https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-mail/mail-account-settings.34481/
|
|
|
|
Returns a types.Packet struct
|
|
Example: p := createPacket("create", 1, "test", true, "Test@mail_123", "plain", "inout")
|
|
*/
|
|
|
|
func (s *server) changeOwnership() (string, error) {
|
|
config, err := s.getSSHConfig()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
conn, err := ssh.Dial("tcp", s.sshAddress, config)
|
|
if err != nil {
|
|
log.Fatal("Failed to dial: ", err)
|
|
return "", err
|
|
}
|
|
defer conn.Close()
|
|
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
fmt.Println(err.Error())
|
|
return "", err
|
|
}
|
|
defer session.Close()
|
|
|
|
//command := "chown -R " + s.MailDirOwner + ":" + s.MailDirGroup + " /var/qmail/mailnames/ververis.eu/test;"
|
|
command := "chown -R " + s.server.MailDirOwner + ":" + s.server.MailDirGroup + " " + s.server.MaildirPath + "/test;"
|
|
|
|
out, err := session.CombinedOutput(command)
|
|
if err != nil {
|
|
fmt.Println("remote execution CMD failed", err)
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
//End server implementation
|
|
|
|
func createPacket(siteId int, action ApiAction, name, password string, enabled bool, passwordType PasswordType, antivirType AntivirType) types.Packet {
|
|
a := &types.Action{}
|
|
a.Filter = types.Filter{
|
|
SiteID: siteId,
|
|
}
|
|
|
|
a.Filter.MailName = types.MailName{
|
|
Name: name,
|
|
Mailbox: &types.MailBox{Enabled: enabled},
|
|
Password: &types.Password{Value: password, Type: string(passwordType)},
|
|
Antivir: &types.Antivir{Antivir: string(antivirType)},
|
|
}
|
|
a.XMLName.Local = string(action)
|
|
p := types.Packet{
|
|
Mail: types.Mail{
|
|
Action: a,
|
|
},
|
|
}
|
|
//fmt.Println(CreateXMLString(p))
|
|
return p
|
|
}
|
|
|
|
func basicAuth(username, password string) string {
|
|
auth := username + ":" + password
|
|
return base64.StdEncoding.EncodeToString([]byte(auth))
|
|
}
|