cgpapi

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 15 Imported by: 0

README

go-cgp-api

A Go client for the CommuniGate Pro PWD/CLI protocol: the TCP session used to authenticate against a CommuniGate Pro server and then issue CLI administration commands, as documented at CLI.html (command grammar and semantics). The PWD login layer itself is not covered by that document and is implemented here from empirical observation of a live server.

import cgpapi "github.com/gmyzovsky/go-cgp-api"

Data-bearing command arguments and responses use go-cgp-data, the sibling package implementing CommuniGate Pro's Data Format.

This is a from-scratch Go design, not a port of any particular prior client's interface - see Design notes below for what that means concretely.

Connecting

Dial opens a connection, authenticates, and returns a ready-to-use *Client:

c, err := cgpapi.Dial(ctx, cgpapi.Options{
    Addr:     "cgpro.example.com:106",
    Login:    "admin@example.com",
    Password: "hunter2",
})
if err != nil {
    log.Fatal(err)
}
defer c.Close(ctx)

Options.SecureLogin selects the authentication method (PlainLogin, APOPLogin, CRAMMD5Login); left at its zero value (AutoSecureLogin), it picks APOP over an unencrypted transport or PLAIN once TLS is already established. Options.TLS selects NoTLS (default), ImplicitTLS (TLS from the first byte, e.g. on port 1106), or StartTLS (plaintext connect, then the STLS command upgrades the connection before authenticating).

A connection dropped unexpectedly is transparently reopened and re-authenticated on the next call, using the same Options, unless Client.Close was called explicitly - after Close, every method returns ErrClosed instead of reconnecting.

A Client is not safe for concurrent use by multiple goroutines: the protocol is a single stateful request/response session over one TCP connection. Use a separate Client per goroutine, or synchronize access to a shared Client externally.

Sending commands

Client.Send runs any already-formatted CLI command line and returns its decoded response, if the command produced one:

v, err := c.Send(ctx, "GETVERSION")
// v is a cgpdata.Value (here, a cgpdata.String) on a data-bearing
// response, nil on a plain success with no data, and err is non-nil
// (typically a *cgpapi.ResponseError) on failure.

Build data-bearing tokens with cgpdata.Marshal:

name, err := cgpdata.Marshal(cgpdata.String("alice@example.com"))
if err != nil {
    log.Fatal(err)
}
v, err := c.Send(ctx, "SetAccountType "+string(name)+" MultiMailbox")

Send already covers every CLI command, documented or not - it is a deliberately dumb pipe, since the command grammar mixes bare keywords, data tokens, arrays, and dictionaries differently per command (see CLI.html). Typed wrapper methods for specific commands build on top of Send and are added incrementally, one command category at a time - see Coverage below.

Typed wrapper methods

Where a typed wrapper exists, it follows one consistent shape across every command: an Input/Output struct pair per method.

out, err := c.CreateAccount(ctx, &cgpapi.CreateAccountInput{
    AccountName: "alice@example.com",
    AccountType: "MultiMailbox",
    Settings: cgpdata.Dictionary{
        {Key: "RealName", Value: cgpdata.String("Alice Doe")},
    },
})
out, err := c.GetAccountSettings(ctx, &cgpapi.GetAccountSettingsInput{
    AccountName: "alice@example.com",
})
realName, _ := out.Settings.Get("RealName")

Coverage

Implemented so far: connect/authenticate (PLAIN, APOP, CRAM-MD5), Send, ChangePassword, and a representative slice of the "Account Administration" category (ListDomainObjects, ListAccounts, ListDomainTelnums, CreateAccount, RenameAccount, DeleteAccount, GetAccountSettings, GetAccountEffectiveSettings, UpdateAccountSettings, SetAccountSettings, SetAccountPassword, VerifyAccountPassword) - the template other command categories (Domain, Group, Mailbox, Alerts, ...) follow as they're ported. Everything else in CLI.html is reachable today through Send.

Design notes

  • Every method returns (T, error). Protocol failures surface as *cgpapi.ResponseError (Code, Message, Command), checkable via errors.As. Command is always just the command's first word, even for commands that carry a credential as an argument (PASS, SetAccountPassword, ...) - so an error never echoes a secret.
  • context.Context is threaded through every network-touching method, in place of a fixed connect/read timeout option.
  • No config equivalent to a UTF8 toggle is needed: Go strings are UTF-8 byte sequences natively.
  • Options.TLSConfig's ServerName (SNI) is derived from Options.Addr's host part for both ImplicitTLS and StartTLS, but only when that host is an actual hostname, not an IP literal - SNI is only valid for hostnames per RFC 6066.

Testing

go test ./...

Tests run against a minimal in-process fake PWD/CLI server (fakeserver_test.go) rather than a live CommuniGate Pro instance.

Documentation

Overview

Package cgpapi implements a client for the CommuniGate Pro PWD/CLI protocol: the TCP session used to authenticate against a CommuniGate Pro server and then issue CLI administration commands, documented at https://doc.communigatepro.ru/development/CLI.html (command grammar and semantics). The PWD login layer itself is not covered by that document and is implemented here from empirical observation of a live server.

Connecting

Dial opens a connection, authenticates using PLAIN (USER/PASS), APOP, or CRAM-MD5, and returns a ready-to-use Client:

c, err := cgpapi.Dial(ctx, cgpapi.Options{
	Addr:     "cgpro.example.com:106",
	Login:    "admin@example.com",
	Password: "hunter2",
})

A connection dropped unexpectedly is transparently reopened and re-authenticated on the next call, using the same Options, unless Client.Close was called explicitly - after Close, every method returns ErrClosed instead of reconnecting.

A Client is not safe for concurrent use by multiple goroutines: the protocol is a single stateful request/response session over one TCP connection. Use a separate Client per goroutine, or synchronize access to a shared Client externally.

Sending commands

Client.Send runs any already-formatted CLI command line - built using github.com/gmyzovsky/go-cgp-data to encode any data-bearing token the command's own grammar requires - and returns its decoded response value, if the command produced one. Send already covers every CLI command, documented or not. Typed wrapper methods for specific commands (e.g. Client.CreateAccount) build on top of Send and are added incrementally, one command category at a time.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("cgpapi: client is closed")

ErrClosed is returned by every Client method called after Close, instead of the transparent auto-reconnect that would otherwise happen on a dropped connection.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a connection to a CommuniGate Pro PWD/CLI server. Create one with Dial.

A Client is not safe for concurrent use by multiple goroutines: the underlying protocol is a single stateful request/response session over one TCP connection. Use a separate Client per goroutine, or synchronize access to a shared Client externally.

func Dial

func Dial(ctx context.Context, opts Options) (*Client, error)

Dial connects to and authenticates with the server described by opts, and returns a ready-to-use Client.

Example
// A real program dials a live CommuniGate Pro server; this example
// dials a minimal fake one standing in for it, so it can run
// deterministically without network access.
srv, err := startFakeServer("200 fake PWD Server ready", func(line string) []string {
	if r, ok := stdLoginPlain(line); ok {
		return r
	}
	return []string{"200 OK"}
})
if err != nil {
	fmt.Println("error:", err)
	return
}
defer srv.ln.Close()

ctx := context.Background()
c, err := Dial(ctx, Options{
	Addr:        srv.Addr(),
	Login:       "admin@example.com",
	Password:    "hunter2",
	SecureLogin: PlainLogin,
})
if err != nil {
	fmt.Println("error:", err)
	return
}
defer c.Close(ctx)

fmt.Println("connected")
Output:
connected

func (*Client) ChangePassword

func (c *Client) ChangePassword(ctx context.Context, newPassword string) error

ChangePassword changes the password of the currently authenticated account, via the PWD/CLI NEWPASS command.

func (*Client) Close

func (c *Client) Close(ctx context.Context) error

Close ends the session, sending QUIT if the connection is currently open, and marks the Client closed: subsequent calls return ErrClosed instead of transparently reconnecting. Call Dial again (or build a new Client) to resume use.

func (*Client) CreateAccount

func (c *Client) CreateAccount(ctx context.Context, in *CreateAccountInput) (*CreateAccountOutput, error)

CreateAccount runs the CREATEACCOUNT command.

Example
srv, err := startFakeServer("200 fake PWD Server ready", func(line string) []string {
	if r, ok := stdLoginPlain(line); ok {
		return r
	}
	return []string{"200 OK"}
})
if err != nil {
	fmt.Println("error:", err)
	return
}
defer srv.ln.Close()

ctx := context.Background()
c, err := Dial(ctx, Options{
	Addr:        srv.Addr(),
	Login:       "admin@example.com",
	Password:    "hunter2",
	SecureLogin: PlainLogin,
})
if err != nil {
	fmt.Println("error:", err)
	return
}
defer c.Close(ctx)

_, err = c.CreateAccount(ctx, &CreateAccountInput{
	AccountName: "alice@example.com",
	AccountType: "MultiMailbox",
})
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println("account created")
Output:
account created

func (*Client) DeleteAccount

func (c *Client) DeleteAccount(ctx context.Context, in *DeleteAccountInput) (*DeleteAccountOutput, error)

DeleteAccount runs the DELETEACCOUNT command.

func (*Client) GetAccountEffectiveSettings

GetAccountEffectiveSettings runs the GETACCOUNTEFFECTIVESETTINGS command.

func (*Client) GetAccountSettings

func (c *Client) GetAccountSettings(ctx context.Context, in *GetAccountSettingsInput) (*GetAccountSettingsOutput, error)

GetAccountSettings runs the GETACCOUNTSETTINGS command.

func (*Client) LastCommand

func (c *Client) LastCommand() string

LastCommand returns the first word of the most recently sent command line, for debugging - never the full line, so it never echoes a credential argument.

func (*Client) ListAccounts

func (c *Client) ListAccounts(ctx context.Context, in *ListAccountsInput) (*ListAccountsOutput, error)

ListAccounts runs the LISTACCOUNTS command.

func (*Client) ListDomainObjects

func (c *Client) ListDomainObjects(ctx context.Context, in *ListDomainObjectsInput) (*ListDomainObjectsOutput, error)

ListDomainObjects runs the LISTDOMAINOBJECTS command.

func (*Client) ListDomainTelnums

func (c *Client) ListDomainTelnums(ctx context.Context, in *ListDomainTelnumsInput) (*ListDomainTelnumsOutput, error)

ListDomainTelnums runs the LISTDOMAINTELNUMS command.

func (*Client) RenameAccount

func (c *Client) RenameAccount(ctx context.Context, in *RenameAccountInput) (*RenameAccountOutput, error)

RenameAccount runs the RENAMEACCOUNT command.

func (*Client) Send

func (c *Client) Send(ctx context.Context, line string) (cgpdata.Value, error)

Send runs line - a single, already-formatted CLI command line, using github.com/gmyzovsky/go-cgp-data to encode any data-bearing token it contains - against the server. It returns the decoded response value for a data-bearing (201) response, nil for a plain success (200) with no data, and a non-nil error for anything else, including a connection failure.

Send is a deliberately dumb pipe: it does not parse or generate CLI command grammar itself, since that grammar mixes bare keywords, data tokens, arrays, and dictionaries differently per command (see https://doc.communigatepro.ru/development/CLI.html). Building the right line for a specific command is the caller's job today, and typed wrapper methods' job for the commands that have one.

Example
srv, err := startFakeServer("200 fake PWD Server ready", func(line string) []string {
	if r, ok := stdLoginPlain(line); ok {
		return r
	}
	if commandVerb(line) == "GETVERSION" {
		return []string{`201 "6.4.1"`}
	}
	return []string{"200 OK"}
})
if err != nil {
	fmt.Println("error:", err)
	return
}
defer srv.ln.Close()

ctx := context.Background()
c, err := Dial(ctx, Options{
	Addr:        srv.Addr(),
	Login:       "admin@example.com",
	Password:    "hunter2",
	SecureLogin: PlainLogin,
})
if err != nil {
	fmt.Println("error:", err)
	return
}
defer c.Close(ctx)

v, err := c.Send(ctx, "GETVERSION")
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(v)
Output:
6.4.1

func (*Client) SetAccountPassword

func (c *Client) SetAccountPassword(ctx context.Context, in *SetAccountPasswordInput) (*SetAccountPasswordOutput, error)

SetAccountPassword runs the SETACCOUNTPASSWORD command.

func (*Client) SetAccountSettings

func (c *Client) SetAccountSettings(ctx context.Context, in *SetAccountSettingsInput) (*SetAccountSettingsOutput, error)

SetAccountSettings runs the SETACCOUNTSETTINGS command.

func (*Client) UpdateAccountSettings

func (c *Client) UpdateAccountSettings(ctx context.Context, in *UpdateAccountSettingsInput) (*UpdateAccountSettingsOutput, error)

UpdateAccountSettings runs the UPDATEACCOUNTSETTINGS command.

func (*Client) VerifyAccountPassword

func (c *Client) VerifyAccountPassword(ctx context.Context, in *VerifyAccountPasswordInput) (*VerifyAccountPasswordOutput, error)

VerifyAccountPassword runs the VERIFYACCOUNTPASSWORD command; a non-nil error (typically a *ResponseError) means the password did not verify.

type CreateAccountInput

type CreateAccountInput struct {
	AccountName string             // required
	AccountType string             // optional bare keyword, e.g. "MultiMailbox"
	External    bool               // optional
	Settings    cgpdata.Dictionary // optional
}

CreateAccountInput creates a new account.

type CreateAccountOutput

type CreateAccountOutput struct{}

CreateAccountOutput holds the result of CreateAccount.

type DeleteAccountInput

type DeleteAccountInput struct {
	AccountName string // required
}

DeleteAccountInput deletes an account.

type DeleteAccountOutput

type DeleteAccountOutput struct{}

DeleteAccountOutput holds the result of DeleteAccount.

type GetAccountEffectiveSettingsInput

type GetAccountEffectiveSettingsInput struct {
	AccountName string // required
}

GetAccountEffectiveSettingsInput reads an account's settings merged with every applicable Domain/Cluster/Server default.

type GetAccountEffectiveSettingsOutput

type GetAccountEffectiveSettingsOutput struct {
	Settings cgpdata.Dictionary
}

GetAccountEffectiveSettingsOutput holds the result of GetAccountEffectiveSettings.

type GetAccountSettingsInput

type GetAccountSettingsInput struct {
	AccountName string // required
}

GetAccountSettingsInput reads an account's own stored settings.

type GetAccountSettingsOutput

type GetAccountSettingsOutput struct {
	Settings cgpdata.Dictionary
}

GetAccountSettingsOutput holds the result of GetAccountSettings.

type ListAccountsInput

type ListAccountsInput struct {
	DomainName string // optional; empty lists across every accessible domain
}

ListAccountsInput lists accounts, optionally restricted to one domain.

type ListAccountsOutput

type ListAccountsOutput struct {
	Accounts cgpdata.Array
}

ListAccountsOutput holds the result of ListAccounts.

type ListDomainObjectsInput

type ListDomainObjectsInput struct {
	DomainName string // required
	Limit      int    // required; the server returns at most this many objects per call
	Filter     string // optional
	What       string // optional bare keyword restricting which object kinds are returned
	Cookie     string // optional; continues a previous paged listing
}

ListDomainObjectsInput lists Directory objects (accounts, forwarders, group objects, ...) within a domain, optionally filtered, in pages bounded by Limit.

type ListDomainObjectsOutput

type ListDomainObjectsOutput struct {
	Objects cgpdata.Array
}

ListDomainObjectsOutput holds the result of ListDomainObjects.

type ListDomainTelnumsInput

type ListDomainTelnumsInput struct {
	DomainName string // required
	Limit      int    // required
	Filter     string // optional
}

ListDomainTelnumsInput lists telephone numbers registered within a domain, optionally filtered, in pages bounded by Limit.

type ListDomainTelnumsOutput

type ListDomainTelnumsOutput struct {
	Telnums cgpdata.Array
}

ListDomainTelnumsOutput holds the result of ListDomainTelnums.

type Options

type Options struct {
	// Addr is the "host:port" of the PWD/CLI server. Required.
	Addr string
	// Login and Password authenticate the session. Both required.
	// Password is kept for the Client's whole lifetime (not just
	// during the initial handshake) to support transparent
	// auto-reconnect.
	Login    string
	Password string

	// TLS selects the transport security mode. Defaults to NoTLS.
	TLS TLSMode
	// TLSConfig is used for both ImplicitTLS and StartTLS connections.
	// A nil value uses a zero-value tls.Config. ServerName, when unset
	// and Addr's host is not an IP literal, is derived from Addr for
	// both modes.
	TLSConfig *tls.Config

	// SecureLogin selects the authentication method. Defaults to
	// AutoSecureLogin.
	SecureLogin SecureLogin

	// DialTimeout bounds the initial TCP connect (and, for
	// ImplicitTLS, the TLS handshake). Zero means no timeout beyond
	// what ctx itself imposes.
	DialTimeout time.Duration
}

Options configures Dial.

type RenameAccountInput

type RenameAccountInput struct {
	OldAccountName string // required
	NewAccountName string // required
	StoragePath    string // optional
}

RenameAccountInput renames an account, optionally relocating its storage.

type RenameAccountOutput

type RenameAccountOutput struct{}

RenameAccountOutput holds the result of RenameAccount.

type Response

type Response struct {
	Code    string
	Message string
}

Response is a parsed PWD/CLI protocol status line: a three-digit status code (or "+" for a SASL continuation) plus its message text.

type ResponseError

type ResponseError struct {
	Code    string // e.g. "515"; empty if the server sent an unparseable status line
	Message string
	Command string
}

ResponseError reports a CommuniGate Pro PWD/CLI server response indicating failure: a status code outside the 2xx/3xx success range.

Command is only the command's first word (e.g. "SetAccountPassword", "PASS"), never the full line - several commands legitimately carry credentials as arguments (PASS, SetAccountPassword, ...), and the full line is deliberately never captured in an error to avoid leaking one into logs.

func (*ResponseError) Error

func (e *ResponseError) Error() string

type SecureLogin

type SecureLogin int

SecureLogin selects the authentication method Dial uses.

const (
	// AutoSecureLogin picks APOP when the transport is not (yet)
	// encrypted, or PLAIN once TLS is already established (ImplicitTLS
	// or StartTLS): APOP works under any server-side password-storage
	// policy, and sending PLAIN in the clear is only avoided when the
	// transport isn't already encrypting it anyway. This is the zero
	// value/default; an explicit SecureLogin value is always honored
	// exactly as given, on any transport.
	AutoSecureLogin SecureLogin = iota
	PlainLogin
	APOPLogin
	CRAMMD5Login
)

func (SecureLogin) String

func (s SecureLogin) String() string

type SetAccountPasswordInput

type SetAccountPasswordInput struct {
	AccountName string // required
	NewPassword string // required
	Method      string // optional bare keyword, e.g. "CLEAR"
	Check       bool   // optional: verify NewPassword meets the domain's password-strength policy before setting it
}

SetAccountPasswordInput sets an account's password.

type SetAccountPasswordOutput

type SetAccountPasswordOutput struct{}

SetAccountPasswordOutput holds the result of SetAccountPassword.

type SetAccountSettingsInput

type SetAccountSettingsInput struct {
	AccountName string             // required
	Settings    cgpdata.Dictionary // required
}

SetAccountSettingsInput replaces an account's entire stored settings with Settings.

type SetAccountSettingsOutput

type SetAccountSettingsOutput struct{}

SetAccountSettingsOutput holds the result of SetAccountSettings.

type TLSMode

type TLSMode int

TLSMode selects how (or whether) a Client establishes transport security with the server.

const (
	// NoTLS connects in the clear. This is the zero value/default.
	NoTLS TLSMode = iota
	// ImplicitTLS establishes a TLS session before the PWD greeting is
	// read, for servers that expect TLS from the first byte
	// (conventionally offered on port 1106).
	ImplicitTLS
	// StartTLS connects in the clear, reads the greeting, sends the
	// STLS command, and upgrades the existing connection to TLS before
	// authenticating.
	StartTLS
)

func (TLSMode) String

func (m TLSMode) String() string

type UpdateAccountSettingsInput

type UpdateAccountSettingsInput struct {
	AccountName string             // required
	Settings    cgpdata.Dictionary // required
}

UpdateAccountSettingsInput merges Settings into an account's existing stored settings, leaving keys not present in Settings unchanged.

type UpdateAccountSettingsOutput

type UpdateAccountSettingsOutput struct{}

UpdateAccountSettingsOutput holds the result of UpdateAccountSettings.

type VerifyAccountPasswordInput

type VerifyAccountPasswordInput struct {
	AccountName string // required
	Password    string // required
}

VerifyAccountPasswordInput checks a candidate password against an account's actual one, without changing it.

type VerifyAccountPasswordOutput

type VerifyAccountPasswordOutput struct{}

VerifyAccountPasswordOutput holds the result of VerifyAccountPassword.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL