mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-22 11:59:40 +01:00
implement mailto callbacks
This commit is contained in:
parent
25f8b15232
commit
89ae261739
148
irc/accounts.go
148
irc/accounts.go
@ -4,27 +4,32 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircfmt"
|
|
||||||
"github.com/oragono/oragono/irc/caps"
|
"github.com/oragono/oragono/irc/caps"
|
||||||
"github.com/oragono/oragono/irc/passwd"
|
"github.com/oragono/oragono/irc/passwd"
|
||||||
"github.com/oragono/oragono/irc/sno"
|
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
keyAccountExists = "account.exists %s"
|
keyAccountExists = "account.exists %s"
|
||||||
keyAccountVerified = "account.verified %s"
|
keyAccountVerified = "account.verified %s"
|
||||||
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
keyAccountCallback = "account.callback %s"
|
||||||
keyAccountRegTime = "account.registered.time %s"
|
keyAccountVerificationCode = "account.verificationcode %s"
|
||||||
keyAccountCredentials = "account.credentials %s"
|
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyAccountRegTime = "account.registered.time %s"
|
||||||
|
keyAccountCredentials = "account.credentials %s"
|
||||||
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// everything about accounts is persistent; therefore, the database is the authoritative
|
// everything about accounts is persistent; therefore, the database is the authoritative
|
||||||
@ -51,7 +56,7 @@ func NewAccountManager(server *Server) *AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) buildNickToAccountIndex() {
|
func (am *AccountManager) buildNickToAccountIndex() {
|
||||||
if am.server.AccountConfig().NickReservation.Enabled {
|
if !am.server.AccountConfig().NickReservation.Enabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +111,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
|
|
||||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||||
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||||
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||||
|
|
||||||
var creds AccountCredentials
|
var creds AccountCredentials
|
||||||
@ -134,6 +141,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
credStr := string(credText)
|
credStr := string(credText)
|
||||||
|
|
||||||
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
|
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
|
||||||
|
|
||||||
var setOptions *buntdb.SetOptions
|
var setOptions *buntdb.SetOptions
|
||||||
ttl := am.server.AccountConfig().Registration.VerifyTimeout
|
ttl := am.server.AccountConfig().Registration.VerifyTimeout
|
||||||
@ -159,6 +167,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
tx.Set(accountNameKey, account, setOptions)
|
tx.Set(accountNameKey, account, setOptions)
|
||||||
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
||||||
tx.Set(credentialsKey, credStr, setOptions)
|
tx.Set(credentialsKey, credStr, setOptions)
|
||||||
|
tx.Set(callbackKey, callbackSpec, setOptions)
|
||||||
if certfp != "" {
|
if certfp != "" {
|
||||||
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
||||||
}
|
}
|
||||||
@ -169,7 +178,69 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
|
||||||
|
if err != nil {
|
||||||
|
am.Unregister(casefoldedAccount)
|
||||||
|
return errCallbackFailed
|
||||||
|
} else {
|
||||||
|
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
_, _, err = tx.Set(verificationCodeKey, code, setOptions)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
||||||
|
if callbackNamespace == "*" || callbackNamespace == "none" {
|
||||||
|
return "", nil
|
||||||
|
} else if callbackNamespace == "mailto" {
|
||||||
|
return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
|
||||||
|
} else {
|
||||||
|
return "", errors.New(fmt.Sprintf("Callback not implemented: %s", callbackNamespace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
|
||||||
|
config := am.server.AccountConfig().Registration.Callbacks.Mailto
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
rand.Read(buf)
|
||||||
|
code = hex.EncodeToString(buf)
|
||||||
|
|
||||||
|
subject := config.VerifyMessageSubject
|
||||||
|
if subject == "" {
|
||||||
|
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
||||||
|
}
|
||||||
|
messageStrings := []string{
|
||||||
|
fmt.Sprintf("From: %s\r\n", config.Sender),
|
||||||
|
fmt.Sprintf("To: %s\r\n", callbackValue),
|
||||||
|
fmt.Sprintf("Subject: %s\r\n", subject),
|
||||||
|
"\r\n", // end headers, begin message body
|
||||||
|
fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
|
||||||
|
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
|
||||||
|
"\r\n",
|
||||||
|
client.t("To verify your account, issue one of these commands:") + "\r\n",
|
||||||
|
fmt.Sprintf("/ACC VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
||||||
|
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
var message []byte
|
||||||
|
for i := 0; i < len(messageStrings); i++ {
|
||||||
|
message = append(message, []byte(messageStrings[i])...)
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
|
||||||
|
var auth smtp.Auth
|
||||||
|
if config.Username != "" && config.Password != "" {
|
||||||
|
auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this will never send the password in plaintext over a nonlocal link,
|
||||||
|
// but it might send the email in plaintext, regardless of the value of
|
||||||
|
// config.TLS.InsecureSkipVerify
|
||||||
|
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
|
||||||
|
if err != nil {
|
||||||
|
am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) Verify(client *Client, account string, code string) error {
|
func (am *AccountManager) Verify(client *Client, account string, code string) error {
|
||||||
@ -182,6 +253,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
|||||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||||
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||||
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||||
|
|
||||||
var raw rawClientAccount
|
var raw rawClientAccount
|
||||||
@ -190,7 +263,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
|||||||
am.serialCacheUpdateMutex.Lock()
|
am.serialCacheUpdateMutex.Lock()
|
||||||
defer am.serialCacheUpdateMutex.Unlock()
|
defer am.serialCacheUpdateMutex.Unlock()
|
||||||
|
|
||||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
||||||
if err == errAccountDoesNotExist {
|
if err == errAccountDoesNotExist {
|
||||||
return errAccountDoesNotExist
|
return errAccountDoesNotExist
|
||||||
@ -200,15 +273,29 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
|||||||
return errAccountAlreadyVerified
|
return errAccountAlreadyVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add code verification here
|
// actually verify the code
|
||||||
// return errAccountVerificationFailed if it fails
|
// a stored code of "" means a none callback / no code required
|
||||||
|
success := false
|
||||||
|
storedCode, err := tx.Get(verificationCodeKey)
|
||||||
|
if err == nil {
|
||||||
|
// this is probably unnecessary
|
||||||
|
if storedCode == "" || subtle.ConstantTimeCompare([]byte(code), []byte(storedCode)) == 1 {
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
return errAccountVerificationInvalidCode
|
||||||
|
}
|
||||||
|
|
||||||
// verify the account
|
// verify the account
|
||||||
tx.Set(verifiedKey, "1", nil)
|
tx.Set(verifiedKey, "1", nil)
|
||||||
|
// don't need the code anymore
|
||||||
|
tx.Delete(verificationCodeKey)
|
||||||
// re-set all other keys, removing the TTL
|
// re-set all other keys, removing the TTL
|
||||||
tx.Set(accountKey, "1", nil)
|
tx.Set(accountKey, "1", nil)
|
||||||
tx.Set(accountNameKey, raw.Name, nil)
|
tx.Set(accountNameKey, raw.Name, nil)
|
||||||
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
|
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
|
||||||
|
tx.Set(callbackKey, raw.Callback, nil)
|
||||||
tx.Set(credentialsKey, raw.Credentials, nil)
|
tx.Set(credentialsKey, raw.Credentials, nil)
|
||||||
|
|
||||||
var creds AccountCredentials
|
var creds AccountCredentials
|
||||||
@ -295,6 +382,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
|||||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||||
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||||
|
|
||||||
_, e := tx.Get(accountKey)
|
_, e := tx.Get(accountKey)
|
||||||
if e == buntdb.ErrNotFound {
|
if e == buntdb.ErrNotFound {
|
||||||
@ -302,15 +390,11 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Name, err = tx.Get(accountNameKey); err != nil {
|
result.Name, _ = tx.Get(accountNameKey)
|
||||||
return
|
result.RegisteredAt, _ = tx.Get(registeredTimeKey)
|
||||||
}
|
result.Credentials, _ = tx.Get(credentialsKey)
|
||||||
if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil {
|
result.Callback, _ = tx.Get(callbackKey)
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.Credentials, err = tx.Get(credentialsKey); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, e = tx.Get(verifiedKey); e == nil {
|
if _, e = tx.Get(verifiedKey); e == nil {
|
||||||
result.Verified = true
|
result.Verified = true
|
||||||
}
|
}
|
||||||
@ -328,6 +412,8 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||||
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||||
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||||
|
|
||||||
var clients []*Client
|
var clients []*Client
|
||||||
@ -343,6 +429,8 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
tx.Delete(accountNameKey)
|
tx.Delete(accountNameKey)
|
||||||
tx.Delete(verifiedKey)
|
tx.Delete(verifiedKey)
|
||||||
tx.Delete(registeredTimeKey)
|
tx.Delete(registeredTimeKey)
|
||||||
|
tx.Delete(callbackKey)
|
||||||
|
tx.Delete(verificationCodeKey)
|
||||||
credText, err = tx.Get(credentialsKey)
|
credText, err = tx.Get(credentialsKey)
|
||||||
tx.Delete(credentialsKey)
|
tx.Delete(credentialsKey)
|
||||||
return nil
|
return nil
|
||||||
@ -484,6 +572,7 @@ type rawClientAccount struct {
|
|||||||
Name string
|
Name string
|
||||||
RegisteredAt string
|
RegisteredAt string
|
||||||
Credentials string
|
Credentials string
|
||||||
|
Callback string
|
||||||
Verified bool
|
Verified bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,10 +590,6 @@ func (client *Client) LoginToAccount(account string) {
|
|||||||
|
|
||||||
client.SetAccountName(casefoldedAccount)
|
client.SetAccountName(casefoldedAccount)
|
||||||
client.nickTimer.Touch()
|
client.nickTimer.Touch()
|
||||||
|
|
||||||
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, casefoldedAccount))
|
|
||||||
|
|
||||||
//TODO(dan): This should output the AccountNotify message instead of the sasl accepted function below.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutOfAccount logs the client out of their current account.
|
// LogoutOfAccount logs the client out of their current account.
|
||||||
@ -518,18 +603,9 @@ func (client *Client) LogoutOfAccount() {
|
|||||||
client.nickTimer.Touch()
|
client.nickTimer.Touch()
|
||||||
|
|
||||||
// dispatch account-notify
|
// dispatch account-notify
|
||||||
|
// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else
|
||||||
for friend := range client.Friends(caps.AccountNotify) {
|
for friend := range client.Friends(caps.AccountNotify) {
|
||||||
friend.Send(nil, client.nickMaskString, "ACCOUNT", "*")
|
friend.Send(nil, client.nickMaskString, "ACCOUNT", "*")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// successfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
|
|
||||||
func (client *Client) successfulSaslAuth(rb *ResponseBuffer) {
|
|
||||||
rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, client.AccountName(), fmt.Sprintf("You are now logged in as %s", client.AccountName()))
|
|
||||||
rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful"))
|
|
||||||
|
|
||||||
// dispatch account-notify
|
|
||||||
for friend := range client.Friends(caps.AccountNotify) {
|
|
||||||
friend.Send(nil, client.nickMaskString, "ACCOUNT", client.AccountName())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,25 +9,27 @@ import "errors"
|
|||||||
|
|
||||||
// Runtime Errors
|
// Runtime Errors
|
||||||
var (
|
var (
|
||||||
errAccountAlreadyRegistered = errors.New("Account already exists")
|
errAccountAlreadyRegistered = errors.New("Account already exists")
|
||||||
errAccountCreation = errors.New("Account could not be created")
|
errAccountCreation = errors.New("Account could not be created")
|
||||||
errAccountDoesNotExist = errors.New("Account does not exist")
|
errAccountDoesNotExist = errors.New("Account does not exist")
|
||||||
errAccountVerificationFailed = errors.New("Account verification failed")
|
errAccountVerificationFailed = errors.New("Account verification failed")
|
||||||
errAccountUnverified = errors.New("Account is not yet verified")
|
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
||||||
errAccountAlreadyVerified = errors.New("Account is already verified")
|
errAccountUnverified = errors.New("Account is not yet verified")
|
||||||
errAccountInvalidCredentials = errors.New("Invalid account credentials")
|
errAccountAlreadyVerified = errors.New("Account is already verified")
|
||||||
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
errAccountInvalidCredentials = errors.New("Invalid account credentials")
|
||||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
errCallbackFailed = errors.New("Account verification could not be sent")
|
||||||
errChannelNameInUse = errors.New("Channel name in use")
|
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
||||||
errInvalidChannelName = errors.New("Invalid channel name")
|
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||||
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
errChannelNameInUse = errors.New("Channel name in use")
|
||||||
errNickMissing = errors.New("nick missing")
|
errInvalidChannelName = errors.New("Invalid channel name")
|
||||||
errNicknameInUse = errors.New("nickname in use")
|
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
||||||
errNicknameReserved = errors.New("nickname is reserved")
|
errNickMissing = errors.New("nick missing")
|
||||||
errNoExistingBan = errors.New("Ban does not exist")
|
errNicknameInUse = errors.New("nickname in use")
|
||||||
errNoSuchChannel = errors.New("No such channel")
|
errNicknameReserved = errors.New("nickname is reserved")
|
||||||
errRenamePrivsNeeded = errors.New("Only chanops can rename channels")
|
errNoExistingBan = errors.New("Ban does not exist")
|
||||||
errSaslFail = errors.New("SASL failed")
|
errNoSuchChannel = errors.New("No such channel")
|
||||||
|
errRenamePrivsNeeded = errors.New("Only chanops can rename channels")
|
||||||
|
errSaslFail = errors.New("SASL failed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socket Errors
|
// Socket Errors
|
||||||
|
134
irc/handlers.go
134
irc/handlers.go
@ -35,12 +35,18 @@ import (
|
|||||||
|
|
||||||
// ACC [REGISTER|VERIFY] ...
|
// ACC [REGISTER|VERIFY] ...
|
||||||
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
// make sure reg is enabled
|
||||||
|
if !server.AccountConfig().Registration.Enabled {
|
||||||
|
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
subcommand := strings.ToLower(msg.Params[0])
|
subcommand := strings.ToLower(msg.Params[0])
|
||||||
|
|
||||||
if subcommand == "register" {
|
if subcommand == "register" {
|
||||||
return accRegisterHandler(server, client, msg, rb)
|
return accRegisterHandler(server, client, msg, rb)
|
||||||
} else if subcommand == "verify" {
|
} else if subcommand == "verify" {
|
||||||
rb.Notice(client.t("VERIFY is not yet implemented"))
|
return accVerifyHandler(server, client, msg, rb)
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
|
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
|
||||||
}
|
}
|
||||||
@ -48,14 +54,33 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
|
func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
|
||||||
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
callback := strings.ToLower(spec)
|
||||||
// make sure reg is enabled
|
if callback == "*" {
|
||||||
if !server.AccountConfig().Registration.Enabled {
|
callbackNamespace = "*"
|
||||||
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
|
} else if strings.Contains(callback, ":") {
|
||||||
return false
|
callbackValues := strings.SplitN(callback, ":", 2)
|
||||||
|
callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
|
||||||
|
} else {
|
||||||
|
// "the IRC server MAY choose to use mailto as a default"
|
||||||
|
callbackNamespace = "mailto"
|
||||||
|
callbackValue = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure the callback namespace is valid
|
||||||
|
// need to search callback list, maybe look at using a map later?
|
||||||
|
for _, name := range config.Registration.EnabledCallbacks {
|
||||||
|
if callbackNamespace == name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// error value
|
||||||
|
callbackNamespace = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
|
||||||
|
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
// clients can't reg new accounts if they're already logged in
|
// clients can't reg new accounts if they're already logged in
|
||||||
if client.LoggedIntoAccount() {
|
if client.LoggedIntoAccount() {
|
||||||
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
||||||
@ -80,30 +105,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
callback := strings.ToLower(msg.Params[2])
|
callbackSpec := msg.Params[2]
|
||||||
var callbackNamespace, callbackValue string
|
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
|
||||||
|
|
||||||
if callback == "*" {
|
if callbackNamespace == "" {
|
||||||
callbackNamespace = "*"
|
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
|
||||||
} else if strings.Contains(callback, ":") {
|
|
||||||
callbackValues := strings.SplitN(callback, ":", 2)
|
|
||||||
callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
|
|
||||||
} else {
|
|
||||||
callbackNamespace = server.AccountConfig().Registration.EnabledCallbacks[0]
|
|
||||||
callbackValue = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the callback namespace is valid
|
|
||||||
// need to search callback list, maybe look at using a map later?
|
|
||||||
var callbackValid bool
|
|
||||||
for _, name := range server.AccountConfig().Registration.EnabledCallbacks {
|
|
||||||
if callbackNamespace == name {
|
|
||||||
callbackValid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !callbackValid {
|
|
||||||
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, client.t("Callback namespace is not supported"))
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,14 +171,66 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, casefoldedAccount, client.t("Account created"))
|
sendSuccessfulRegResponse(client, rb, false)
|
||||||
client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
|
} else {
|
||||||
client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
|
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
|
||||||
server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString))
|
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
|
||||||
|
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch callback
|
return false
|
||||||
rb.Notice(fmt.Sprintf("We should dispatch a real callback here to %s:%s", callbackNamespace, callbackValue))
|
}
|
||||||
|
|
||||||
|
func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
|
||||||
|
if forNS {
|
||||||
|
rb.Notice(client.t("Account created"))
|
||||||
|
}
|
||||||
|
rb.Add(nil, client.server.name, RPL_REGISTRATION_SUCCESS, client.nick, client.AccountName(), client.t("Account created"))
|
||||||
|
sendSuccessfulSaslAuth(client, rb, forNS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSuccessfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
|
||||||
|
func sendSuccessfulSaslAuth(client *Client, rb *ResponseBuffer, forNS bool) {
|
||||||
|
account := client.AccountName()
|
||||||
|
rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account, fmt.Sprintf("You are now logged in as %s", account))
|
||||||
|
rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful"))
|
||||||
|
|
||||||
|
if forNS {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch account-notify
|
||||||
|
for friend := range client.Friends(caps.AccountNotify) {
|
||||||
|
friend.Send(nil, client.nickMaskString, "ACCOUNT", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, account))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACC VERIFY <accountname> <auth_code>
|
||||||
|
func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
account := strings.TrimSpace(msg.Params[1])
|
||||||
|
err := server.accounts.Verify(client, account, msg.Params[2])
|
||||||
|
|
||||||
|
var code string
|
||||||
|
var message string
|
||||||
|
|
||||||
|
if err == errAccountVerificationInvalidCode {
|
||||||
|
code = ERR_ACCOUNT_INVALID_VERIFY_CODE
|
||||||
|
message = err.Error()
|
||||||
|
} else if err == errAccountAlreadyVerified {
|
||||||
|
code = ERR_ACCOUNT_ALREADY_VERIFIED
|
||||||
|
message = err.Error()
|
||||||
|
} else if err != nil {
|
||||||
|
code = ERR_UNKNOWNERROR
|
||||||
|
message = errAccountVerificationFailed.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
sendSuccessfulRegResponse(client, rb, false)
|
||||||
|
} else {
|
||||||
|
rb.Add(nil, server.name, code, client.nick, account, client.t(message))
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -301,7 +359,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
client.successfulSaslAuth(rb)
|
sendSuccessfulSaslAuth(client, rb, false)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +387,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
client.successfulSaslAuth(rb)
|
sendSuccessfulSaslAuth(client, rb, false)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
147
irc/nickserv.go
147
irc/nickserv.go
@ -6,16 +6,20 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircfmt"
|
|
||||||
"github.com/oragono/oragono/irc/sno"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: "email" is an oversimplification here; it's actually any callback, e.g.,
|
||||||
|
// person@example.com, mailto:person@example.com, tel:16505551234.
|
||||||
const nickservHelp = `NickServ lets you register and log into a user account.
|
const nickservHelp = `NickServ lets you register and log into a user account.
|
||||||
|
|
||||||
To register an account:
|
To register an account:
|
||||||
/NS REGISTER username [password]
|
/NS REGISTER username email [password]
|
||||||
Leave out [password] if you're registering using your client certificate fingerprint.
|
Leave out [password] if you're registering using your client certificate fingerprint.
|
||||||
|
The server may or may not allow you to register anonymously (by sending * as your
|
||||||
|
email address).
|
||||||
|
|
||||||
|
To verify an account (if you were sent a verification code):
|
||||||
|
/NS VERIFY username code
|
||||||
|
|
||||||
To unregister an account:
|
To unregister an account:
|
||||||
/NS UNREGISTER [username]
|
/NS UNREGISTER [username]
|
||||||
@ -53,19 +57,14 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb
|
|||||||
}
|
}
|
||||||
} else if command == "register" {
|
} else if command == "register" {
|
||||||
// get params
|
// get params
|
||||||
username, passphrase := extractParam(params)
|
username, afterUsername := extractParam(params)
|
||||||
|
email, passphrase := extractParam(afterUsername)
|
||||||
// fail out if we need to
|
server.nickservRegisterHandler(client, username, email, passphrase, rb)
|
||||||
if username == "" {
|
} else if command == "verify" {
|
||||||
rb.Notice(client.t("No username supplied"))
|
username, code := extractParam(params)
|
||||||
return
|
server.nickservVerifyHandler(client, username, code, rb)
|
||||||
}
|
|
||||||
|
|
||||||
server.nickservRegisterHandler(client, username, passphrase, rb)
|
|
||||||
} else if command == "identify" {
|
} else if command == "identify" {
|
||||||
// get params
|
|
||||||
username, passphrase := extractParam(params)
|
username, passphrase := extractParam(params)
|
||||||
|
|
||||||
server.nickservIdentifyHandler(client, username, passphrase, rb)
|
server.nickservIdentifyHandler(client, username, passphrase, rb)
|
||||||
} else if command == "unregister" {
|
} else if command == "unregister" {
|
||||||
username, _ := extractParam(params)
|
username, _ := extractParam(params)
|
||||||
@ -98,6 +97,10 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfname == client.Account() {
|
||||||
|
client.server.accounts.Logout(client)
|
||||||
|
}
|
||||||
|
|
||||||
err = server.accounts.Unregister(cfname)
|
err = server.accounts.Unregister(cfname)
|
||||||
if err == errAccountDoesNotExist {
|
if err == errAccountDoesNotExist {
|
||||||
rb.Notice(client.t(err.Error()))
|
rb.Notice(client.t(err.Error()))
|
||||||
@ -108,18 +111,41 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) nickservRegisterHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
|
func (server *Server) nickservVerifyHandler(client *Client, username string, code string, rb *ResponseBuffer) {
|
||||||
certfp := client.certfp
|
err := server.accounts.Verify(client, username, code)
|
||||||
if passphrase == "" && certfp == "" {
|
|
||||||
rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
|
var errorMessage string
|
||||||
|
if err == errAccountVerificationInvalidCode || err == errAccountAlreadyVerified {
|
||||||
|
errorMessage = err.Error()
|
||||||
|
} else if err != nil {
|
||||||
|
errorMessage = errAccountVerificationFailed.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorMessage != "" {
|
||||||
|
rb.Notice(client.t(errorMessage))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendSuccessfulRegResponse(client, rb, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) nickservRegisterHandler(client *Client, username, email, passphrase string, rb *ResponseBuffer) {
|
||||||
if !server.AccountConfig().Registration.Enabled {
|
if !server.AccountConfig().Registration.Enabled {
|
||||||
rb.Notice(client.t("Account registration has been disabled"))
|
rb.Notice(client.t("Account registration has been disabled"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
rb.Notice(client.t("No username supplied"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certfp := client.certfp
|
||||||
|
if passphrase == "" && certfp == "" {
|
||||||
|
rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if client.LoggedIntoAccount() {
|
if client.LoggedIntoAccount() {
|
||||||
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
||||||
server.accounts.Logout(client)
|
server.accounts.Logout(client)
|
||||||
@ -129,26 +155,42 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config := server.AccountConfig()
|
||||||
|
var callbackNamespace, callbackValue string
|
||||||
|
noneCallbackAllowed := false
|
||||||
|
for _, callback := range(config.Registration.EnabledCallbacks) {
|
||||||
|
if callback == "*" {
|
||||||
|
noneCallbackAllowed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// XXX if ACC REGISTER allows registration with the `none` callback, then ignore
|
||||||
|
// any callback that was passed here (to avoid confusion in the case where the ircd
|
||||||
|
// has no mail server configured). otherwise, register using the provided callback:
|
||||||
|
if noneCallbackAllowed {
|
||||||
|
callbackNamespace = "*"
|
||||||
|
} else {
|
||||||
|
callbackNamespace, callbackValue = parseCallback(email, config)
|
||||||
|
if callbackNamespace == "" {
|
||||||
|
rb.Notice(client.t("Registration requires a valid e-mail address"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get and sanitise account name
|
// get and sanitise account name
|
||||||
account := strings.TrimSpace(username)
|
account := strings.TrimSpace(username)
|
||||||
casefoldedAccount, err := CasefoldName(account)
|
|
||||||
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
|
|
||||||
if err != nil || username == "*" {
|
|
||||||
rb.Notice(client.t("Account name is not valid"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// account could not be created and relevant numerics have been dispatched, abort
|
err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp)
|
||||||
if err != nil {
|
|
||||||
if err != errAccountCreation {
|
|
||||||
rb.Notice(client.t("Account registration failed"))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = server.accounts.Verify(client, casefoldedAccount, "")
|
if callbackNamespace == "*" {
|
||||||
|
err = server.accounts.Verify(client, account, "")
|
||||||
|
if err == nil {
|
||||||
|
sendSuccessfulRegResponse(client, rb, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
|
||||||
|
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
|
||||||
|
rb.Notice(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// details could not be stored and relevant numerics have been dispatched, abort
|
// details could not be stored and relevant numerics have been dispatched, abort
|
||||||
@ -162,11 +204,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
|||||||
rb.Notice(client.t(errMsg))
|
rb.Notice(client.t(errMsg))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rb.Notice(client.t("Account created"))
|
|
||||||
rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
|
|
||||||
rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
|
|
||||||
server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
|
func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
|
||||||
@ -176,31 +213,23 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginSuccessful := false
|
||||||
|
|
||||||
// try passphrase
|
// try passphrase
|
||||||
if username != "" && passphrase != "" {
|
if username != "" && passphrase != "" {
|
||||||
// keep it the same as in the ACC CREATE stage
|
err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
|
||||||
accountName, err := CasefoldName(username)
|
loginSuccessful = (err == nil)
|
||||||
if err != nil {
|
|
||||||
rb.Notice(client.t("Could not login with your username/password"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = server.accounts.AuthenticateByPassphrase(client, accountName, passphrase)
|
|
||||||
if err == nil {
|
|
||||||
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// try certfp
|
// try certfp
|
||||||
if client.certfp != "" {
|
if !loginSuccessful && client.certfp != "" {
|
||||||
err := server.accounts.AuthenticateByCertFP(client)
|
err := server.accounts.AuthenticateByCertFP(client)
|
||||||
if err == nil {
|
loginSuccessful = (err == nil)
|
||||||
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
|
|
||||||
// TODO more notices?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
|
if loginSuccessful {
|
||||||
|
sendSuccessfulSaslAuth(client, rb, true)
|
||||||
|
} else {
|
||||||
|
rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
oragono.yaml
13
oragono.yaml
@ -151,7 +151,18 @@ accounts:
|
|||||||
# callbacks to allow
|
# callbacks to allow
|
||||||
enabled-callbacks:
|
enabled-callbacks:
|
||||||
- none # no verification needed, will instantly register successfully
|
- none # no verification needed, will instantly register successfully
|
||||||
|
|
||||||
|
# example configuration for sending verification emails via a local mail relay
|
||||||
|
# callbacks:
|
||||||
|
# mailto:
|
||||||
|
# server: localhost
|
||||||
|
# port: 25
|
||||||
|
# tls:
|
||||||
|
# enabled: false
|
||||||
|
# username: ""
|
||||||
|
# password: ""
|
||||||
|
# sender: "admin@my.network"
|
||||||
|
|
||||||
# allow multiple account registrations per connection
|
# allow multiple account registrations per connection
|
||||||
# this is for testing purposes and shouldn't be allowed on real networks
|
# this is for testing purposes and shouldn't be allowed on real networks
|
||||||
allow-multiple-per-connection: false
|
allow-multiple-per-connection: false
|
||||||
|
Loading…
Reference in New Issue
Block a user