diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50de8a7..0f5f084 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -96,6 +96,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Mail account write endpoints (#114, #13 write slice):
+ `kasapi-cli mail accounts add
--password [field flags]`,
+ `… update [field flags]` and
+ `… delete ` wire `add_mailaccount` / `update_mailaccount`
+ / `delete_mailaccount`. `update` and `delete` are gated by the #109
+ confirmation prompt; `add` is reversible and not prompted. All three
+ honour `--dry-run` (#132) and emit a #131 audit record; the password
+ is redacted in both. `add` splits the address on the last `@` into
+ the `local_part` / `domain_part` KAS expects and takes no
+ `mail_login` — KAS generates the login (e.g. `m0000001`) and echoes
+ it in `ReturnInfo`, which the command prints. The Y/N/text toggles
+ and XLIST folder names default to the KAS API's own defaults, so a
+ bare `add --password ` is a complete create. `update`
+ sends only the explicitly-set flags (keyed on cobra `Changed`), adds
+ the `--active` (`is_active`) toggle, and its `--password` maps to
+ `mail_new_password` (the `_new_password` split the
+ database/ftpuser/sambauser slices carry) rather than the add-only
+ `mail_password`. `responder` is passed through verbatim ("N", "Y" or
+ a `|` timestamp range). `delete_mailaccount`'s prompt
+ uses the louder verb "permanently delete" — it drops the mailbox and
+ every message in it (the same data-loss emphasis as
+ `delete_database`).
+
- `database.InProgressFalse` / `database.InProgressTrue` package
constants for the literal `"FALSE"` / `"TRUE"` strings the KAS API
uses to encode the async-write flag, so mapping code and tests
diff --git a/docs/cli/kasapi-cli_mail.md b/docs/cli/kasapi-cli_mail.md
index 7198155..00c15cb 100644
--- a/docs/cli/kasapi-cli_mail.md
+++ b/docs/cli/kasapi-cli_mail.md
@@ -29,7 +29,7 @@ Inspect mail accounts and filters; inspect and manage forwards and mailing lists
### SEE ALSO
* [kasapi-cli](kasapi-cli.md) - Command-line client for the All-Inkl KAS API
-* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect mail accounts (get_mailaccounts)
+* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect and manage mail accounts (get/add/update/delete_mailaccount)
* [kasapi-cli mail filters](kasapi-cli_mail_filters.md) - Inspect mail standard filters (get_mailstandardfilter)
* [kasapi-cli mail forwards](kasapi-cli_mail_forwards.md) - Inspect and manage mail forwards (get/add/update/delete_mailforward)
* [kasapi-cli mail lists](kasapi-cli_mail_lists.md) - Inspect and manage mailing lists (get/add/update/delete_mailinglist)
diff --git a/docs/cli/kasapi-cli_mail_accounts.md b/docs/cli/kasapi-cli_mail_accounts.md
index 197ec20..b683a5c 100644
--- a/docs/cli/kasapi-cli_mail_accounts.md
+++ b/docs/cli/kasapi-cli_mail_accounts.md
@@ -1,6 +1,6 @@
## kasapi-cli mail accounts
-Inspect mail accounts (get_mailaccounts)
+Inspect and manage mail accounts (get/add/update/delete_mailaccount)
### Options
@@ -29,6 +29,9 @@ Inspect mail accounts (get_mailaccounts)
### SEE ALSO
* [kasapi-cli mail](kasapi-cli_mail.md) - Inspect mail accounts and filters; inspect and manage forwards and mailing lists
+* [kasapi-cli mail accounts add](kasapi-cli_mail_accounts_add.md) - Create a mail account (add_mailaccount; the login is generated by KAS)
+* [kasapi-cli mail accounts delete](kasapi-cli_mail_accounts_delete.md) - Permanently delete a mail account and all messages in it (delete_mailaccount)
* [kasapi-cli mail accounts get](kasapi-cli_mail_accounts_get.md) - Show details for a single mail account (get_mailaccounts with mail_login)
* [kasapi-cli mail accounts list](kasapi-cli_mail_accounts_list.md) - List all mail accounts (get_mailaccounts)
+* [kasapi-cli mail accounts update](kasapi-cli_mail_accounts_update.md) - Replace mutable fields of a mail account (update_mailaccount)
diff --git a/docs/cli/kasapi-cli_mail_accounts_add.md b/docs/cli/kasapi-cli_mail_accounts_add.md
new file mode 100644
index 0000000..44557f2
--- /dev/null
+++ b/docs/cli/kasapi-cli_mail_accounts_add.md
@@ -0,0 +1,62 @@
+## kasapi-cli mail accounts add
+
+Create a mail account (add_mailaccount; the login is generated by KAS)
+
+### Synopsis
+
+Create a mail account via add_mailaccount. The address is split on the
+last '@' into the local_part / domain_part KAS expects, and KAS
+generates the mail login (e.g. m0000001), which the command prints on
+success.
+
+The Y/N/text toggles and XLIST folder names default to the KAS API's
+own defaults, so "accounts add info@example.com --password " is a
+complete create; override any field with its flag.
+
+```
+kasapi-cli mail accounts add --password [field flags] [flags]
+```
+
+### Options
+
+```
+ --allow-nets string restrict access to these IP/CIDR networks (empty = no restriction)
+ --copy-address string BCC copy recipient address(es)
+ -h, --help help for add
+ --password string initial mailbox password (required)
+ --responder string auto-responder: "N", "Y", or a "|" timestamp range (default "N")
+ --responder-content-type string auto-responder body format (html|text) (default "text")
+ --responder-displayname string auto-responder sender display name
+ --responder-text string auto-responder message body
+ --sender-alias string permitted FROM alias address(es)
+ --webmail-autologin string allow KAS-to-webmail auto-login (Y|N) (default "Y")
+ --xlist-archiv string XLIST archive folder name (default "Archive")
+ --xlist-drafts string XLIST drafts folder name (default "Drafts")
+ --xlist-enabled string enable XLIST special-folder mapping (Y|N) (default "Y")
+ --xlist-sent string XLIST sent-items folder name (default "Sent")
+ --xlist-spam string XLIST spam folder name (default "Spam")
+ --xlist-trash string XLIST trash folder name (default "Trash")
+```
+
+### Options inherited from parent commands
+
+```
+ --audit-log string append a JSON-Lines audit record for each write action to this file (also KAS_AUDIT_LOG); a logfmt line always goes to stderr regardless
+ --auth-data string KAS auth data (overrides config and KAS_AUTHDATA)
+ --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE.
+ --config string path to the kasapi-cli config file (overrides the default location)
+ --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output
+ --login string KAS login (overrides config and KAS_LOGIN)
+ --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls.
+ -o, --output string output format: json|yaml|table (default table)
+ --profile string profile to select from the config file (overrides default_profile)
+ --session-lifetime int session_lifetime in seconds passed to KasAuth (1..30000); 0 keeps the server default. Requires auth_type=session.
+ --session-update-lifetime string session_update_lifetime passed to KasAuth ('Y' = sliding window, 'N' = fixed). Empty omits the parameter. Requires auth_type=session.
+ -v, --verbose enable verbose logging on stderr
+ -y, --yes skip confirmation prompts on destructive operations
+```
+
+### SEE ALSO
+
+* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect and manage mail accounts (get/add/update/delete_mailaccount)
+
diff --git a/docs/cli/kasapi-cli_mail_accounts_delete.md b/docs/cli/kasapi-cli_mail_accounts_delete.md
new file mode 100644
index 0000000..6ab52c9
--- /dev/null
+++ b/docs/cli/kasapi-cli_mail_accounts_delete.md
@@ -0,0 +1,36 @@
+## kasapi-cli mail accounts delete
+
+Permanently delete a mail account and all messages in it (delete_mailaccount)
+
+```
+kasapi-cli mail accounts delete [flags]
+```
+
+### Options
+
+```
+ -h, --help help for delete
+```
+
+### Options inherited from parent commands
+
+```
+ --audit-log string append a JSON-Lines audit record for each write action to this file (also KAS_AUDIT_LOG); a logfmt line always goes to stderr regardless
+ --auth-data string KAS auth data (overrides config and KAS_AUTHDATA)
+ --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE.
+ --config string path to the kasapi-cli config file (overrides the default location)
+ --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output
+ --login string KAS login (overrides config and KAS_LOGIN)
+ --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls.
+ -o, --output string output format: json|yaml|table (default table)
+ --profile string profile to select from the config file (overrides default_profile)
+ --session-lifetime int session_lifetime in seconds passed to KasAuth (1..30000); 0 keeps the server default. Requires auth_type=session.
+ --session-update-lifetime string session_update_lifetime passed to KasAuth ('Y' = sliding window, 'N' = fixed). Empty omits the parameter. Requires auth_type=session.
+ -v, --verbose enable verbose logging on stderr
+ -y, --yes skip confirmation prompts on destructive operations
+```
+
+### SEE ALSO
+
+* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect and manage mail accounts (get/add/update/delete_mailaccount)
+
diff --git a/docs/cli/kasapi-cli_mail_accounts_get.md b/docs/cli/kasapi-cli_mail_accounts_get.md
index 7ad0815..9ee0ebd 100644
--- a/docs/cli/kasapi-cli_mail_accounts_get.md
+++ b/docs/cli/kasapi-cli_mail_accounts_get.md
@@ -32,5 +32,5 @@ kasapi-cli mail accounts get [flags]
### SEE ALSO
-* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect mail accounts (get_mailaccounts)
+* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect and manage mail accounts (get/add/update/delete_mailaccount)
diff --git a/docs/cli/kasapi-cli_mail_accounts_list.md b/docs/cli/kasapi-cli_mail_accounts_list.md
index a127654..5b01d1c 100644
--- a/docs/cli/kasapi-cli_mail_accounts_list.md
+++ b/docs/cli/kasapi-cli_mail_accounts_list.md
@@ -32,5 +32,5 @@ kasapi-cli mail accounts list [flags]
### SEE ALSO
-* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect mail accounts (get_mailaccounts)
+* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect and manage mail accounts (get/add/update/delete_mailaccount)
diff --git a/docs/cli/kasapi-cli_mail_accounts_update.md b/docs/cli/kasapi-cli_mail_accounts_update.md
new file mode 100644
index 0000000..2af6460
--- /dev/null
+++ b/docs/cli/kasapi-cli_mail_accounts_update.md
@@ -0,0 +1,52 @@
+## kasapi-cli mail accounts update
+
+Replace mutable fields of a mail account (update_mailaccount)
+
+```
+kasapi-cli mail accounts update [field flags] [flags]
+```
+
+### Options
+
+```
+ --active string mailbox status (Y|N)
+ --allow-nets string restrict access to these IP/CIDR networks
+ --copy-address string BCC copy recipient address(es)
+ -h, --help help for update
+ --password string replacement mailbox password (sent as mail_new_password)
+ --responder string auto-responder: "N", "Y", or a "|" timestamp range
+ --responder-content-type string auto-responder body format (html|text)
+ --responder-displayname string auto-responder sender display name
+ --responder-text string auto-responder message body
+ --sender-alias string permitted FROM alias address(es)
+ --webmail-autologin string allow KAS-to-webmail auto-login (Y|N)
+ --xlist-archiv string XLIST archive folder name
+ --xlist-drafts string XLIST drafts folder name
+ --xlist-enabled string enable XLIST special-folder mapping (Y|N)
+ --xlist-sent string XLIST sent-items folder name
+ --xlist-spam string XLIST spam folder name
+ --xlist-trash string XLIST trash folder name
+```
+
+### Options inherited from parent commands
+
+```
+ --audit-log string append a JSON-Lines audit record for each write action to this file (also KAS_AUDIT_LOG); a logfmt line always goes to stderr regardless
+ --auth-data string KAS auth data (overrides config and KAS_AUTHDATA)
+ --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE.
+ --config string path to the kasapi-cli config file (overrides the default location)
+ --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output
+ --login string KAS login (overrides config and KAS_LOGIN)
+ --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls.
+ -o, --output string output format: json|yaml|table (default table)
+ --profile string profile to select from the config file (overrides default_profile)
+ --session-lifetime int session_lifetime in seconds passed to KasAuth (1..30000); 0 keeps the server default. Requires auth_type=session.
+ --session-update-lifetime string session_update_lifetime passed to KasAuth ('Y' = sliding window, 'N' = fixed). Empty omits the parameter. Requires auth_type=session.
+ -v, --verbose enable verbose logging on stderr
+ -y, --yes skip confirmation prompts on destructive operations
+```
+
+### SEE ALSO
+
+* [kasapi-cli mail accounts](kasapi-cli_mail_accounts.md) - Inspect and manage mail accounts (get/add/update/delete_mailaccount)
+
diff --git a/docs/usage/destructive-writes.md b/docs/usage/destructive-writes.md
index 4385e97..cbcc96e 100644
--- a/docs/usage/destructive-writes.md
+++ b/docs/usage/destructive-writes.md
@@ -30,8 +30,8 @@ the deviations.
sinks.
- Subcommands whose KAS action generates the login server-side
(`add_ftpuser`, `add_sambauser`, `add_ddnsuser`, `add_database`,
- `add_mailinglist`, `add_mailforward`) print the generated identifier
- on success.
+ `add_mailinglist`, `add_mailforward`, `add_mailaccount`) print the
+ generated identifier on success.
## Per-slice deviations
@@ -44,6 +44,7 @@ the deviations.
| [`sambausers`](https://github.com/chmmou/kasapi-cli/issues/120) | Password key splits between actions: `--password` → `samba_password` on add, `samba_new_password` on update. Note: the KAS docs wrongly list `samba_new_password` for the create call; the captured fixture confirms the real key is `samba_password`. |
| [`databases`](https://github.com/chmmou/kasapi-cli/issues/122) | **Louder delete prompt**: `delete_database` uses the verb `"permanently delete"` (vs the bare `"delete"` every other slice uses) because the action drops the database AND every row in it — the loudest data-loss surface of the v0.2.0 write phase. Password key splits between actions: `--password` → `database_password` on add, `database_new_password` on update. `--allowed-hosts` is **optional**: an empty value is the KAS API's documented "any host may connect" wildcard, sent verbatim on the wire. |
| [`ddnsusers`](https://github.com/chmmou/kasapi-cli/issues/121) | **No `_new_password` split**: `--password` maps to `dyndns_password` on both `add` and `update`. `update_ddnsuser` accepts `--target-ipv4` / `--target-ipv6` instead of `add`'s legacy `--target-ip`; the ipv4/ipv6 keys are undocumented in the KAS API docs but verified to work against the live system (the captured update request fixture is authoritative). |
+| [`mail accounts`](https://github.com/chmmou/kasapi-cli/issues/114) | **Louder delete prompt**: `delete_mailaccount` uses `"permanently delete"` (shared with `databases`) — it drops the mailbox AND every message in it. `add` splits the address on the last `@` into `local_part` / `domain_part`; `update`/`delete` address the account by its generated `mail_login`. Password key splits between actions: `--password` → `mail_password` on add, `mail_new_password` on update. `add`'s Y/N/text toggles and XLIST folder names default to the KAS API's own defaults; `--responder` is passed through verbatim (`N`, `Y`, or a `\|` timestamp range). |
## The contract
diff --git a/internal/cli/export_test.go b/internal/cli/export_test.go
index eec5883..4a735ad 100644
--- a/internal/cli/export_test.go
+++ b/internal/cli/export_test.go
@@ -34,8 +34,13 @@ var RevokeSession = revokeSession
// temp session.Store, mirroring the `config use-profile` pattern.
var RunSessionsDelete = runSessionsDelete
-// DatabaseDeleteConfirm exposes the package-private helper that
-// builds the ConfirmAction for delete_database. Tests use it to pin
-// the "permanently delete" loudness adjustment (database is the only
-// slice using that emphatic verb).
-var DatabaseDeleteConfirm = databaseDeleteConfirm
+// DatabaseDeleteConfirm and MailAccountDeleteConfirm expose the
+// package-private helpers that build the delete ConfirmAction for their
+// slices. Tests use them to pin the "permanently delete" loudness
+// adjustment — the two data-loss deletes (a database drops all rows, a
+// mail account drops all stored messages) that use the emphatic verb
+// instead of the bare "delete" every other slice uses.
+var (
+ DatabaseDeleteConfirm = databaseDeleteConfirm
+ MailAccountDeleteConfirm = mailAccountDeleteConfirm
+)
diff --git a/internal/cli/mail.go b/internal/cli/mail.go
index a1d513f..f89325f 100644
--- a/internal/cli/mail.go
+++ b/internal/cli/mail.go
@@ -210,11 +210,14 @@ func newMailFiltersListCmd(opts *RootOptions) *cobra.Command {
func newMailAccountsCmd(opts *RootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "accounts",
- Short: "Inspect mail accounts (get_mailaccounts)",
+ Short: "Inspect and manage mail accounts (get/add/update/delete_mailaccount)",
}
cmd.AddCommand(
newMailAccountsListCmd(opts),
newMailAccountsGetCmd(opts),
+ newMailAccountsAddCmd(opts),
+ newMailAccountsUpdateCmd(opts),
+ newMailAccountsDeleteCmd(opts),
)
return cmd
}
@@ -241,6 +244,265 @@ func newMailAccountsGetCmd(opts *RootOptions) *cobra.Command {
}
}
+// mailAccountAddFlags binds the add_mailaccount request fields. add and
+// update bind disjoint flag sets (same wire-key surface, but each
+// subcommand's --help reflects only its own action semantics, and cobra
+// rejects a wrong-subcommand flag at parse time). The password flag maps
+// to mail_password here (the add-only key); update has its own
+// --password flag that maps to mail_new_password.
+//
+// The Y/N/text toggles and the XLIST folder names default to the values
+// the KAS API documents as its own defaults (and that the captured
+// add_mailaccount request fixture carries), so a bare
+// "accounts add --password " produces a valid create.
+type mailAccountAddFlags struct {
+ password string
+ webmailAutologin string
+ responder string
+ responderContentType string
+ responderDisplayName string
+ responderText string
+ copyAddress string
+ senderAlias string
+ xlistEnabled string
+ xlistSent string
+ xlistDrafts string
+ xlistTrash string
+ xlistSpam string
+ xlistArchiv string
+ allowNets string
+}
+
+func (f *mailAccountAddFlags) bind(cmd *cobra.Command) {
+ fl := cmd.Flags()
+ fl.StringVar(&f.password, "password", "", "initial mailbox password (required)")
+ fl.StringVar(&f.webmailAutologin, "webmail-autologin", "Y", "allow KAS-to-webmail auto-login (Y|N)")
+ fl.StringVar(&f.responder, "responder", "N", `auto-responder: "N", "Y", or a "|" timestamp range`)
+ fl.StringVar(&f.responderContentType, "responder-content-type", "text", "auto-responder body format (html|text)")
+ fl.StringVar(&f.responderDisplayName, "responder-displayname", "", "auto-responder sender display name")
+ fl.StringVar(&f.responderText, "responder-text", "", "auto-responder message body")
+ fl.StringVar(&f.copyAddress, "copy-address", "", "BCC copy recipient address(es)")
+ fl.StringVar(&f.senderAlias, "sender-alias", "", "permitted FROM alias address(es)")
+ fl.StringVar(&f.xlistEnabled, "xlist-enabled", "Y", "enable XLIST special-folder mapping (Y|N)")
+ fl.StringVar(&f.xlistSent, "xlist-sent", "Sent", "XLIST sent-items folder name")
+ fl.StringVar(&f.xlistDrafts, "xlist-drafts", "Drafts", "XLIST drafts folder name")
+ fl.StringVar(&f.xlistTrash, "xlist-trash", "Trash", "XLIST trash folder name")
+ fl.StringVar(&f.xlistSpam, "xlist-spam", "Spam", "XLIST spam folder name")
+ fl.StringVar(&f.xlistArchiv, "xlist-archiv", "Archive", "XLIST archive folder name")
+ fl.StringVar(&f.allowNets, "allow-nets", "", "restrict access to these IP/CIDR networks (empty = no restriction)")
+}
+
+func (f *mailAccountAddFlags) spec(local, domain string) mailaccount.Spec {
+ return mailaccount.Spec{
+ LocalPart: local,
+ DomainPart: domain,
+ Password: f.password,
+ WebmailAutologin: f.webmailAutologin,
+ Responder: f.responder,
+ ResponderContentType: f.responderContentType,
+ ResponderDisplayName: f.responderDisplayName,
+ ResponderText: f.responderText,
+ CopyAddress: f.copyAddress,
+ SenderAlias: f.senderAlias,
+ XListEnabled: f.xlistEnabled,
+ XListSent: f.xlistSent,
+ XListDrafts: f.xlistDrafts,
+ XListTrash: f.xlistTrash,
+ XListSpam: f.xlistSpam,
+ XListArchiv: f.xlistArchiv,
+ AllowNets: f.allowNets,
+ }
+}
+
+func newMailAccountsAddCmd(opts *RootOptions) *cobra.Command {
+ f := &mailAccountAddFlags{}
+ cmd := &cobra.Command{
+ Use: "add --password [field flags]",
+ Short: "Create a mail account (add_mailaccount; the login is generated by KAS)",
+ Long: `Create a mail account via add_mailaccount. The address is split on the
+last '@' into the local_part / domain_part KAS expects, and KAS
+generates the mail login (e.g. m0000001), which the command prints on
+success.
+
+The Y/N/text toggles and XLIST folder names default to the KAS API's
+own defaults, so "accounts add info@example.com --password " is a
+complete create; override any field with its flag.`,
+ Args: cobra.ExactArgs(1),
+ RunE: runWriteE(opts, func(args []string) (writeSpec, error) {
+ local, domain, err := splitMailAddress(args[0])
+ if err != nil {
+ return writeSpec{}, err
+ }
+ if f.password == "" {
+ return writeSpec{}, fmt.Errorf("--password is required")
+ }
+ s := f.spec(local, domain)
+ return writeSpec{
+ action: "add_mailaccount",
+ destructive: false,
+ confirm: ConfirmAction{Verb: "create", Resource: "mail account", ID: args[0]},
+ params: mailaccount.AddParams(s),
+ dispatch: func(c *api.Client, ctx context.Context) (string, error) {
+ login, derr := mailaccount.NewClient(c).Add(ctx, s)
+ if derr != nil {
+ return "", derr
+ }
+ return "created mail account " + login, nil
+ },
+ }, nil
+ }),
+ }
+ f.bind(cmd)
+ return cmd
+}
+
+// mailAccountUpdateFlags binds the update_mailaccount mutable surface.
+// Disjoint from mailAccountAddFlags: update has no local/domain (the
+// account is addressed by its generated mail_login), adds the --active
+// toggle (is_active), and its --password maps to mail_new_password (the
+// update key) rather than the add-only mail_password. No flag carries a
+// default — only the flags the user explicitly sets are sent, so an
+// unset flag means "leave unchanged".
+type mailAccountUpdateFlags struct {
+ password string
+ active string
+ webmailAutologin string
+ responder string
+ responderContentType string
+ responderDisplayName string
+ responderText string
+ copyAddress string
+ senderAlias string
+ xlistEnabled string
+ xlistSent string
+ xlistDrafts string
+ xlistTrash string
+ xlistSpam string
+ xlistArchiv string
+ allowNets string
+}
+
+func (f *mailAccountUpdateFlags) bind(cmd *cobra.Command) {
+ fl := cmd.Flags()
+ fl.StringVar(&f.password, "password", "", "replacement mailbox password (sent as mail_new_password)")
+ fl.StringVar(&f.active, "active", "", "mailbox status (Y|N)")
+ fl.StringVar(&f.webmailAutologin, "webmail-autologin", "", "allow KAS-to-webmail auto-login (Y|N)")
+ fl.StringVar(&f.responder, "responder", "", `auto-responder: "N", "Y", or a "|" timestamp range`)
+ fl.StringVar(&f.responderContentType, "responder-content-type", "", "auto-responder body format (html|text)")
+ fl.StringVar(&f.responderDisplayName, "responder-displayname", "", "auto-responder sender display name")
+ fl.StringVar(&f.responderText, "responder-text", "", "auto-responder message body")
+ fl.StringVar(&f.copyAddress, "copy-address", "", "BCC copy recipient address(es)")
+ fl.StringVar(&f.senderAlias, "sender-alias", "", "permitted FROM alias address(es)")
+ fl.StringVar(&f.xlistEnabled, "xlist-enabled", "", "enable XLIST special-folder mapping (Y|N)")
+ fl.StringVar(&f.xlistSent, "xlist-sent", "", "XLIST sent-items folder name")
+ fl.StringVar(&f.xlistDrafts, "xlist-drafts", "", "XLIST drafts folder name")
+ fl.StringVar(&f.xlistTrash, "xlist-trash", "", "XLIST trash folder name")
+ fl.StringVar(&f.xlistSpam, "xlist-spam", "", "XLIST spam folder name")
+ fl.StringVar(&f.xlistArchiv, "xlist-archiv", "", "XLIST archive folder name")
+ fl.StringVar(&f.allowNets, "allow-nets", "", "restrict access to these IP/CIDR networks")
+}
+
+// mailAccountUpdateChangedFields collects only the flags the user
+// explicitly set into the update_mailaccount field map (keyed on the
+// mailaccount.Field* constants). Each field is a wholesale replacement
+// and an empty value is a meaningful set, so presence is keyed on cobra
+// Changed, not on the value being non-empty — the same pattern the
+// database/ftpuser updates use. The password flag maps to
+// mail_new_password here (update_mailaccount's key).
+func mailAccountUpdateChangedFields(cmd *cobra.Command, f *mailAccountUpdateFlags) map[string]string {
+ fields := map[string]string{}
+ for _, m := range []struct {
+ flag string
+ key string
+ value string
+ }{
+ {"password", mailaccount.FieldNewPassword, f.password},
+ {"active", mailaccount.FieldIsActive, f.active},
+ {"webmail-autologin", mailaccount.FieldWebmailAutologin, f.webmailAutologin},
+ {"responder", mailaccount.FieldResponder, f.responder},
+ {"responder-content-type", mailaccount.FieldResponderContentType, f.responderContentType},
+ {"responder-displayname", mailaccount.FieldResponderDisplayName, f.responderDisplayName},
+ {"responder-text", mailaccount.FieldResponderText, f.responderText},
+ {"copy-address", mailaccount.FieldCopyAddress, f.copyAddress},
+ {"sender-alias", mailaccount.FieldSenderAlias, f.senderAlias},
+ {"xlist-enabled", mailaccount.FieldXListEnabled, f.xlistEnabled},
+ {"xlist-sent", mailaccount.FieldXListSent, f.xlistSent},
+ {"xlist-drafts", mailaccount.FieldXListDrafts, f.xlistDrafts},
+ {"xlist-trash", mailaccount.FieldXListTrash, f.xlistTrash},
+ {"xlist-spam", mailaccount.FieldXListSpam, f.xlistSpam},
+ {"xlist-archiv", mailaccount.FieldXListArchiv, f.xlistArchiv},
+ {"allow-nets", mailaccount.FieldAllowNets, f.allowNets},
+ } {
+ if cmd.Flags().Changed(m.flag) {
+ fields[m.key] = m.value
+ }
+ }
+ return fields
+}
+
+func newMailAccountsUpdateCmd(opts *RootOptions) *cobra.Command {
+ f := &mailAccountUpdateFlags{}
+ cmd := &cobra.Command{
+ Use: "update [field flags]",
+ Short: "Replace mutable fields of a mail account (update_mailaccount)",
+ Args: cobra.ExactArgs(1),
+ }
+ cmd.RunE = runWriteE(opts, func(args []string) (writeSpec, error) {
+ login := args[0]
+ fields := mailAccountUpdateChangedFields(cmd, f)
+ if len(fields) == 0 {
+ return writeSpec{}, fmt.Errorf("at least one field flag (e.g. --password/--active/--responder) is required")
+ }
+ return writeSpec{
+ action: "update_mailaccount",
+ destructive: true,
+ confirm: ConfirmAction{Verb: "replace the settings of", Resource: "mail account", ID: login},
+ params: mailaccount.UpdateParams(login, fields),
+ dispatch: func(c *api.Client, ctx context.Context) (string, error) {
+ if derr := mailaccount.NewClient(c).Update(ctx, login, fields); derr != nil {
+ return "", derr
+ }
+ return "updated mail account " + login, nil
+ },
+ }, nil
+ })
+ f.bind(cmd)
+ return cmd
+}
+
+// mailAccountDeleteConfirm builds the ConfirmAction shown before
+// delete_mailaccount is dispatched. Like delete_database it uses the
+// louder "permanently delete" verb: deleting a mail account drops the
+// mailbox and every message stored in it, so this is data loss, not
+// just metadata removal. The prompt template adds "This cannot be
+// undone." regardless of the verb.
+func mailAccountDeleteConfirm(login string) ConfirmAction {
+ return ConfirmAction{Verb: "permanently delete", Resource: "mail account", ID: login}
+}
+
+func newMailAccountsDeleteCmd(opts *RootOptions) *cobra.Command {
+ return &cobra.Command{
+ Use: "delete ",
+ Short: "Permanently delete a mail account and all messages in it (delete_mailaccount)",
+ Args: cobra.ExactArgs(1),
+ RunE: runWriteE(opts, func(args []string) (writeSpec, error) {
+ login := args[0]
+ return writeSpec{
+ action: "delete_mailaccount",
+ destructive: true,
+ confirm: mailAccountDeleteConfirm(login),
+ params: mailaccount.DeleteParams(login),
+ dispatch: func(c *api.Client, ctx context.Context) (string, error) {
+ if derr := mailaccount.NewClient(c).Delete(ctx, login); derr != nil {
+ return "", derr
+ }
+ return "deleted mail account " + login, nil
+ },
+ }, nil
+ }),
+ }
+}
+
func newMailForwardsCmd(opts *RootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "forwards",
@@ -257,12 +519,14 @@ func newMailForwardsCmd(opts *RootOptions) *cobra.Command {
}
// splitMailAddress splits "local@domain" on the last '@' into the
-// local_part / domain_part add_mailforward expects. get/update/delete
-// take the full address verbatim; only add needs it decomposed.
+// local_part / domain_part the add_mailforward and add_mailaccount
+// actions expect. Their get/update/delete counterparts take the full
+// address (forwards) or the generated mail_login (accounts) verbatim;
+// only the add path needs the address decomposed.
func splitMailAddress(addr string) (local, domain string, err error) {
at := strings.LastIndex(addr, "@")
if at <= 0 || at == len(addr)-1 {
- return "", "", fmt.Errorf("invalid mail forward address %q: want local@domain", addr)
+ return "", "", fmt.Errorf("invalid mail address %q: want local@domain", addr)
}
return addr[:at], addr[at+1:], nil
}
diff --git a/internal/cli/mail_test.go b/internal/cli/mail_test.go
index 8ce6630..861cc06 100644
--- a/internal/cli/mail_test.go
+++ b/internal/cli/mail_test.go
@@ -43,13 +43,231 @@ func TestMailAccountsHelpListsSubcommands(t *testing.T) {
t.Fatalf("Execute: %v", err)
}
out := buf.String()
- for _, want := range []string{"list", "get"} {
+ for _, want := range []string{"list", "get", "add", "update", "delete"} {
if !strings.Contains(out, want) {
t.Errorf("--help output missing %q\n%s", want, out)
}
}
}
+func TestMailAccountsAddRejectsBadInput(t *testing.T) {
+ t.Parallel()
+ cases := []struct {
+ name string
+ args []string
+ }{
+ {"address without @", []string{"mail", "accounts", "add", "notanemail", "--password", "pw"}},
+ {"missing --password", []string{"mail", "accounts", "add", "info@example.com"}},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ t.Parallel()
+ root, opts := cli.NewRootCmd()
+ root.AddCommand(cli.NewMailCmd(opts))
+ var buf bytes.Buffer
+ root.SetOut(&buf)
+ root.SetErr(&buf)
+ root.SetArgs(c.args)
+ err := root.Execute()
+ if err == nil {
+ t.Fatalf("Execute %v: want error, got nil", c.args)
+ }
+ if cli.CodeFor(err) != cli.ExitUserError {
+ t.Errorf("exit code = %d, want ExitUserError", cli.CodeFor(err))
+ }
+ })
+ }
+}
+
+// update_mailaccount with no field flags is rejected at build time
+// (before any gating or credential resolution) with a user error — the
+// API would otherwise fault nothing_to_do. Mirrors the add input-
+// rejection test.
+func TestMailAccountsUpdateRejectsNoFields(t *testing.T) {
+ t.Parallel()
+ root, opts := cli.NewRootCmd()
+ root.AddCommand(cli.NewMailCmd(opts))
+ var buf bytes.Buffer
+ root.SetOut(&buf)
+ root.SetErr(&buf)
+ root.SetArgs([]string{"mail", "accounts", "update", "m0000001"})
+ err := root.Execute()
+ if err == nil {
+ t.Fatal("Execute: want error for update with no field flags, got nil")
+ }
+ if cli.CodeFor(err) != cli.ExitUserError {
+ t.Errorf("exit code = %d, want ExitUserError", cli.CodeFor(err))
+ }
+}
+
+// The destructive accounts subcommands (update/delete) must refuse on a
+// non-interactive stdin without --yes rather than dispatch unconfirmed.
+func TestMailAccountsDestructiveRefuseNonTTY(t *testing.T) {
+ t.Parallel()
+ for _, args := range [][]string{
+ {"mail", "accounts", "delete", "m0000001"},
+ {"mail", "accounts", "update", "m0000001", "--active", "N"},
+ } {
+ t.Run(args[2], func(t *testing.T) {
+ t.Parallel()
+ root, opts := cli.NewRootCmd()
+ root.AddCommand(cli.NewMailCmd(opts))
+ var buf bytes.Buffer
+ root.SetOut(&buf)
+ root.SetErr(&buf)
+ root.SetIn(strings.NewReader(""))
+ root.SetArgs(append(args,
+ "--login", "w0000000", "--auth-data", "x", "--auth-type", "plain"))
+ err := root.Execute()
+ if err == nil {
+ t.Fatalf("Execute %v: want refusal error, got nil", args)
+ }
+ if !errors.Is(err, cli.ErrConfirmationRequired) {
+ t.Errorf("err = %v, want ErrConfirmationRequired", err)
+ }
+ })
+ }
+}
+
+// delete_mailaccount uses the louder "permanently delete" verb (a
+// deleted mailbox loses every stored message), shared only with
+// delete_database. Pin the verb and the rendered Summary so neither a
+// slice refactor nor a change to the shared ConfirmAction template can
+// silently regress the loudness contract.
+func TestMailAccountsDeleteConfirmIsLouder(t *testing.T) {
+ t.Parallel()
+ a := cli.MailAccountDeleteConfirm("m0000001")
+ if a.Verb != "permanently delete" {
+ t.Errorf("Verb = %q, want %q", a.Verb, "permanently delete")
+ }
+ if a.Resource != "mail account" {
+ t.Errorf("Resource = %q, want %q", a.Resource, "mail account")
+ }
+ want := `About to permanently delete mail account "m0000001". This cannot be undone.`
+ if got := a.Summary(); got != want {
+ t.Errorf("Summary() = %q, want %q", got, want)
+ }
+}
+
+// add_mailaccount is non-destructive (no prompt) but still routes
+// through the dry-run/audit seam. The dry-run preview must split the
+// address into local_part/domain_part, send the add-only mail_password,
+// carry the documented default toggles, and never leak the update-only
+// mail_login / mail_new_password / is_active keys.
+func TestMailAccountsAddDryRunParams(t *testing.T) {
+ t.Parallel()
+ root, opts := cli.NewRootCmd()
+ root.AddCommand(cli.NewMailCmd(opts))
+ var out, errb bytes.Buffer
+ root.SetOut(&out)
+ root.SetErr(&errb)
+ root.SetArgs([]string{
+ "mail", "accounts", "add", "info@example.com", "--password", "s3cret",
+ "--dry-run", "-o", "json",
+ "--login", "w0", "--auth-data", "x", "--auth-type", "plain",
+ })
+ if err := root.Execute(); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ var got struct {
+ Action string `json:"action"`
+ Params map[string]string `json:"params"`
+ }
+ if err := json.Unmarshal(out.Bytes(), &got); err != nil {
+ t.Fatalf("unmarshal preview: %v\nstdout:%s", err, out.String())
+ }
+ if got.Action != "add_mailaccount" {
+ t.Errorf("action = %q, want add_mailaccount", got.Action)
+ }
+ want := map[string]string{
+ "local_part": "info",
+ "domain_part": "example.com",
+ // mail_password is a secret, so the audit/preview redacts its
+ // value — the key is still present but the value is masked.
+ "mail_password": "",
+ "webmail_autologin": "Y",
+ "responder": "N",
+ "mail_xlist_enabled": "Y",
+ "mail_xlist_sent": "Sent",
+ }
+ for k, v := range want {
+ if got.Params[k] != v {
+ t.Errorf("params[%q] = %q, want %q (full: %v)", k, got.Params[k], v, got.Params)
+ }
+ }
+ for _, k := range []string{"mail_login", "mail_new_password", "is_active"} {
+ if _, ok := got.Params[k]; ok {
+ t.Errorf("add params must not contain %q (full: %v)", k, got.Params)
+ }
+ }
+}
+
+// The accounts update field assembly mirrors the lists update: only the
+// flags the user explicitly set are sent (keyed on cobra Changed), the
+// password maps to the update-only mail_new_password key, and --active
+// maps to is_active. --dry-run renders the exact KAS params, so this
+// asserts the assembly end to end without a network call.
+func TestMailAccountsUpdateDryRunFieldAssembly(t *testing.T) {
+ t.Parallel()
+ cases := []struct {
+ name string
+ args []string
+ want map[string]string
+ absent []string
+ }{
+ {
+ "password maps to mail_new_password",
+ []string{"mail", "accounts", "update", "m0000001", "--password", "n3w"},
+ // mail_new_password is a secret → redacted in the preview.
+ map[string]string{"mail_login": "m0000001", "mail_new_password": ""},
+ []string{"mail_password", "is_active", "responder"},
+ },
+ {
+ "active and responder",
+ []string{"mail", "accounts", "update", "m0000001", "--active", "N", "--responder", "Y"},
+ map[string]string{"mail_login": "m0000001", "is_active": "N", "responder": "Y"},
+ []string{"mail_new_password"},
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ t.Parallel()
+ root, opts := cli.NewRootCmd()
+ root.AddCommand(cli.NewMailCmd(opts))
+ var out, errb bytes.Buffer
+ root.SetOut(&out)
+ root.SetErr(&errb)
+ args := append(append([]string{}, c.args...),
+ "--dry-run", "-o", "json",
+ "--login", "w0", "--auth-data", "x", "--auth-type", "plain")
+ root.SetArgs(args)
+ if err := root.Execute(); err != nil {
+ t.Fatalf("Execute %v: %v", args, err)
+ }
+ var got struct {
+ Action string `json:"action"`
+ Params map[string]string `json:"params"`
+ }
+ if err := json.Unmarshal(out.Bytes(), &got); err != nil {
+ t.Fatalf("unmarshal preview: %v\nstdout:%s", err, out.String())
+ }
+ if got.Action != "update_mailaccount" {
+ t.Errorf("action = %q, want update_mailaccount", got.Action)
+ }
+ for k, v := range c.want {
+ if got.Params[k] != v {
+ t.Errorf("params[%q] = %q, want %q (full: %v)", k, got.Params[k], v, got.Params)
+ }
+ }
+ for _, k := range c.absent {
+ if _, ok := got.Params[k]; ok {
+ t.Errorf("params[%q] present, want absent (full: %v)", k, got.Params)
+ }
+ }
+ })
+ }
+}
+
func TestMailForwardsHelpListsSubcommands(t *testing.T) {
t.Parallel()
root, opts := cli.NewRootCmd()
diff --git a/internal/mailaccount/fault_test.go b/internal/mailaccount/fault_test.go
index 2acc0fe..48eb876 100644
--- a/internal/mailaccount/fault_test.go
+++ b/internal/mailaccount/fault_test.go
@@ -6,10 +6,75 @@ import (
"github.com/chmmou/kasapi-cli/internal/testutil"
)
+// TestFaultFixturesDecodeToDocumentedCodes pins every captured
+// mailaccount fault fixture to the documented KAS fault code it carries.
+// AssertFaultFixtures decodes each _response_failed_ file in the module
+// dir to a *soap.FaultError (and checks the code is non-empty); listing
+// every file here additionally asserts the exact code, so the fixtures
+// stay the authoritative contract for the get/add/update/delete actions.
func TestFaultFixturesDecodeToDocumentedCodes(t *testing.T) {
t.Parallel()
+ //nolint:gosec // G101: these map values are KAS fault-code strings (e.g. "webmail_autologin_change_requires_new_password"), not hardcoded credentials.
testutil.AssertFaultFixtures(t, "mailaccount", map[string]string{
- "add_mailaccount_response_failed_copy_adress_like_mailaccount.xml": "copy_adress_like_mailaccount",
- "add_mailaccount_response_failed_copy_adress_syntax_incorrect.xml": "copy_adress_syntax_incorrect",
+ "add_mailaccount_response_failed_copy_adress_like_mailaccount.xml": "copy_adress_like_mailaccount",
+ "add_mailaccount_response_failed_copy_adress_syntax_incorrect.xml": "copy_adress_syntax_incorrect",
+ "add_mailaccount_response_failed_couldnt_get_kas_ressources.xml": "couldnt_get_kas_ressources",
+ "add_mailaccount_response_failed_email_already_exists.xml": "email_already_exists",
+ "add_mailaccount_response_failed_email_domain_doesnt_exist.xml": "email_domain_doesnt_exist",
+ "add_mailaccount_response_failed_email_syntax_incorrect.xml": "email_syntax_incorrect",
+ "add_mailaccount_response_failed_mail_loop_detected.xml": "mail_loop_detected",
+ "add_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml": "mail_xlist_archiv_syntax_incorrect",
+ "add_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml": "mail_xlist_drafts_syntax_incorrect",
+ "add_mailaccount_response_failed_mail_xlist_duplicate_folder.xml": "mail_xlist_duplicate_folder",
+ "add_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml": "mail_xlist_enabled_syntax_incorrect",
+ "add_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml": "mail_xlist_sent_syntax_incorrect",
+ "add_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml": "mail_xlist_spam_syntax_incorrect",
+ "add_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml": "mail_xlist_trash_syntax_incorrect",
+ "add_mailaccount_response_failed_max_emails_reached.xml": "max_emails_reached",
+ "add_mailaccount_response_failed_max_sender_alias_reached.xml": "max_sender_alias_reached",
+ "add_mailaccount_response_failed_missing_parameter.xml": "missing_parameter",
+ "add_mailaccount_response_failed_password_syntax_incorrect.xml": "password_syntax_incorrect",
+ "add_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml": "responder_contentype_syntax_incorrect",
+ "add_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml": "responder_displayname_syntax_incorrect",
+ "add_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml": "responder_not_allowed_for_catchall_adresses",
+ "add_mailaccount_response_failed_responder_startdate_gt_enddate.xml": "responder_startdate_gt_enddate",
+ "add_mailaccount_response_failed_responder_syntax_incorrect.xml": "responder_syntax_incorrect",
+ "add_mailaccount_response_failed_responder_text_is_empty.xml": "responder_text_is_empty",
+ "add_mailaccount_response_failed_sender_alias_domain_in_kas.xml": "sender_alias_domain_in_kas",
+ "add_mailaccount_response_failed_sender_alias_syntax_incorrect.xml": "sender_alias_syntax_incorrect",
+ "add_mailaccount_response_failed_webmail_autologin_syntax_incorrect.xml": "webmail_autologin_syntax_incorrect",
+ "delete_mailaccount_response_failed_in_progress.xml": "in_progress",
+ "delete_mailaccount_response_failed_mail_login_not_found.xml": "mail_login_not_found",
+ "delete_mailaccount_response_failed_mail_loop_detected.xml": "mail_loop_detected",
+ "delete_mailaccount_response_failed_missing_parameter.xml": "missing_parameter",
+ "update_mailaccount_response_failed_copy_adress_like_mailaccount.xml": "copy_adress_like_mailaccount",
+ "update_mailaccount_response_failed_copy_adress_syntax_incorrect.xml": "copy_adress_syntax_incorrect",
+ "update_mailaccount_response_failed_email_already_exists.xml": "email_already_exists",
+ "update_mailaccount_response_failed_email_domain_doesnt_exist.xml": "email_domain_doesnt_exist",
+ "update_mailaccount_response_failed_in_progress.xml": "in_progress",
+ "update_mailaccount_response_failed_is_active_syntax_incorrect.xml": "is_active_syntax_incorrect",
+ "update_mailaccount_response_failed_mail_login_not_found.xml": "mail_login_not_found",
+ "update_mailaccount_response_failed_mail_loop_detected.xml": "mail_loop_detected",
+ "update_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml": "mail_xlist_archiv_syntax_incorrect",
+ "update_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml": "mail_xlist_drafts_syntax_incorrect",
+ "update_mailaccount_response_failed_mail_xlist_duplicate_folder.xml": "mail_xlist_duplicate_folder",
+ "update_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml": "mail_xlist_enabled_syntax_incorrect",
+ "update_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml": "mail_xlist_sent_syntax_incorrect",
+ "update_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml": "mail_xlist_spam_syntax_incorrect",
+ "update_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml": "mail_xlist_trash_syntax_incorrect",
+ "update_mailaccount_response_failed_max_sender_alias_reached.xml": "max_sender_alias_reached",
+ "update_mailaccount_response_failed_missing_parameter.xml": "missing_parameter",
+ "update_mailaccount_response_failed_nothing_to_do.xml": "nothing_to_do",
+ "update_mailaccount_response_failed_password_syntax_incorrect.xml": "password_syntax_incorrect",
+ "update_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml": "responder_contentype_syntax_incorrect",
+ "update_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml": "responder_displayname_syntax_incorrect",
+ "update_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml": "responder_not_allowed_for_catchall_adresses",
+ "update_mailaccount_response_failed_responder_startdate_gt_enddate.xml": "responder_startdate_gt_enddate",
+ "update_mailaccount_response_failed_responder_syntax_incorrect.xml": "responder_syntax_incorrect",
+ "update_mailaccount_response_failed_responder_text_is_empty.xml": "responder_text_is_empty",
+ "update_mailaccount_response_failed_sender_alias_domain_in_kas.xml": "sender_alias_domain_in_kas",
+ "update_mailaccount_response_failed_sender_alias_syntax_incorrect.xml": "sender_alias_syntax_incorrect",
+ "update_mailaccount_response_failed_webmail_autologin_change_requires_new_password.xml": "webmail_autologin_change_requires_new_password",
+ "update_mailaccount_response_failed_webmail_autologin_syntax_incorrect.xml": "webmail_autologin_syntax_incorrect",
})
}
diff --git a/internal/mailaccount/mailaccount.go b/internal/mailaccount/mailaccount.go
index 3167ca6..ef5a6e0 100644
--- a/internal/mailaccount/mailaccount.go
+++ b/internal/mailaccount/mailaccount.go
@@ -64,22 +64,30 @@ type MailAccount struct {
// cli.Tabular.
type MailAccountList []MailAccount
-// Client groups the read endpoints scoped to mail accounts:
-// get_mailaccounts (list and singular).
+// Client groups the read endpoints scoped to mail accounts
+// (get_mailaccounts, list and singular) and the write endpoints
+// add_mailaccount / update_mailaccount / delete_mailaccount (see
+// write.go). The raw Caller is kept alongside the read helper so the
+// write methods can dispatch their own KAS actions through the shared
+// kaswrite seam.
type Client struct {
lg kasread.ListGet[MailAccountList, MailAccount]
+ c Caller
}
// NewClient returns a Client backed by the given Caller.
func NewClient(c Caller) *Client {
- return &Client{lg: kasread.ListGet[MailAccountList, MailAccount]{
- Caller: c,
- Action: "get_mailaccounts",
- Label: "mailaccount",
- ArgName: "login",
- FilterKey: "mail_login",
- Decoder: DecodeMailAccounts,
- }}
+ return &Client{
+ lg: kasread.ListGet[MailAccountList, MailAccount]{
+ Caller: c,
+ Action: "get_mailaccounts",
+ Label: "mailaccount",
+ ArgName: "login",
+ FilterKey: "mail_login",
+ Decoder: DecodeMailAccounts,
+ },
+ c: c,
+ }
}
// List calls get_mailaccounts without parameters and decodes the
diff --git a/internal/mailaccount/mailaccount_test.go b/internal/mailaccount/mailaccount_test.go
index eec8ef5..c389f4f 100644
--- a/internal/mailaccount/mailaccount_test.go
+++ b/internal/mailaccount/mailaccount_test.go
@@ -24,7 +24,7 @@ func TestDecodeMailAccounts(t *testing.T) {
if a.Login != "m0000001" {
t.Errorf("Login = %q, want m0000001", a.Login)
}
- if a.Addresses != "m0000001@example.com" || a.Adresses != "m0000001@example.com" {
+ if a.Addresses != "info@example.com" || a.Adresses != "info@example.com" {
t.Errorf("addresses pair = %q / %q", a.Addresses, a.Adresses)
}
if a.Spamfilter != "pdw,sf" {
@@ -52,11 +52,14 @@ func TestDecodeMailAccountSingular(t *testing.T) {
t.Fatalf("len = %d, want 1", len(got))
}
a := got[0]
- if a.Login == "" {
- t.Errorf("Login empty")
+ if a.Login != "m0000001" {
+ t.Errorf("Login = %q, want m0000001", a.Login)
}
- if a.UsedSpace == 0 {
- t.Errorf("UsedSpace = 0, want non-zero")
+ // The singular fixture is an empty mailbox, so used_mailaccount_space
+ // is a real 0 (xsd:int) — assert the address decodes instead of
+ // pinning a non-zero usage that the fixture no longer carries.
+ if a.Addresses != "info@example.com" {
+ t.Errorf("Addresses = %q, want info@example.com", a.Addresses)
}
}
diff --git a/internal/mailaccount/write.go b/internal/mailaccount/write.go
new file mode 100644
index 0000000..04b6311
--- /dev/null
+++ b/internal/mailaccount/write.go
@@ -0,0 +1,194 @@
+package mailaccount
+
+import (
+ "context"
+ "errors"
+
+ "github.com/chmmou/kasapi-cli/internal/kaswrite"
+)
+
+// ErrUnexpectedReturnString is the shared canonical post-call-contract
+// sentinel, re-exported so errors.Is(err, mailaccount.ErrUnexpectedReturnString)
+// keeps working and the slice stays self-describing. See
+// kaswrite.ErrUnexpectedReturnString for the full contract.
+var ErrUnexpectedReturnString = kaswrite.ErrUnexpectedReturnString
+
+const (
+ addAction = "add_mailaccount"
+ updateAction = "update_mailaccount"
+ deleteAction = "delete_mailaccount"
+)
+
+// The Field-prefixed constants are the KAS request keys
+// update_mailaccount accepts besides the mail_login identifier; each is
+// an optional wholesale replacement, so only the keys the caller
+// explicitly sets are sent and an empty string is a meaningful value
+// rather than "leave unchanged".
+//
+// Two asymmetries against the add path are baked into the wire keys and
+// must not be conflated:
+//
+// - The password key differs by action: add_mailaccount sets the
+// initial password via mail_password, update_mailaccount replaces it
+// via mail_new_password (the same _new_password split as
+// ftpuser/sambauser/database). FieldNewPassword is the update key;
+// add uses its own mail_password key in AddParams.
+// - FieldIsActive (is_active) and FieldLogin (mail_login) exist only on
+// update: add_mailaccount takes no mail_login (the server generates
+// it) and has no is_active toggle (a freshly created account is
+// active).
+//
+// FieldCopyAddress keeps the legacy single-d "copy_adress" spelling
+// because that is the literal wire key both add and update accept
+// (mirrors the mail_adresses/mail_copy_adress legacy keys on the read
+// path).
+const (
+ FieldLogin = "mail_login"
+ FieldNewPassword = "mail_new_password"
+ FieldWebmailAutologin = "webmail_autologin"
+ FieldResponder = "responder"
+ FieldResponderContentType = "mail_responder_content_type"
+ FieldResponderDisplayName = "mail_responder_displayname"
+ FieldResponderText = "responder_text"
+ FieldCopyAddress = "copy_adress"
+ FieldIsActive = "is_active"
+ FieldSenderAlias = "mail_sender_alias"
+ FieldXListEnabled = "mail_xlist_enabled"
+ FieldXListSent = "mail_xlist_sent"
+ FieldXListDrafts = "mail_xlist_drafts"
+ FieldXListTrash = "mail_xlist_trash"
+ FieldXListSpam = "mail_xlist_spam"
+ FieldXListArchiv = "mail_xlist_archiv"
+ FieldAllowNets = "mail_allow_nets"
+)
+
+// Spec carries the add_mailaccount request fields. LocalPart, DomainPart
+// and Password are required by the KAS API and validated as non-empty
+// before any SOAP call so the CLI can surface a fast validation error;
+// the remaining fields are optional and sent verbatim (an empty value is
+// sent as an empty string, matching the captured add_mailaccount request
+// fixture, which carries every key even when blank).
+//
+// add_mailaccount takes no mail_login: the server auto-generates the
+// login and echoes it in ReturnInfo (e.g. "m0000001"), which Add
+// returns. Responder is passed through verbatim — the API accepts the
+// literal strings "N" / "Y" or a "|" timestamp range.
+type Spec struct {
+ LocalPart string
+ DomainPart string
+ Password string
+ WebmailAutologin string
+ Responder string
+ ResponderContentType string
+ ResponderDisplayName string
+ ResponderText string
+ CopyAddress string
+ SenderAlias string
+ XListEnabled string
+ XListSent string
+ XListDrafts string
+ XListTrash string
+ XListSpam string
+ XListArchiv string
+ AllowNets string
+}
+
+// Add creates a mail account (add_mailaccount) and returns the login the
+// server assigns, as echoed in ReturnInfo (e.g. "m0000001").
+//
+// LocalPart, DomainPart and Password are validated before the SOAP call
+// so an obviously incomplete spec fails fast; the remaining syntax
+// validation (email_syntax_incorrect, password_syntax_incorrect,
+// mail_xlist_*_syntax_incorrect, responder_*, …) is left to the API and
+// surfaces verbatim through the Caller.
+func (cl *Client) Add(ctx context.Context, s Spec) (string, error) {
+ switch {
+ case s.LocalPart == "":
+ return "", errors.New("mailaccount: add_mailaccount requires a non-empty local part")
+ case s.DomainPart == "":
+ return "", errors.New("mailaccount: add_mailaccount requires a non-empty domain part")
+ case s.Password == "":
+ return "", errors.New("mailaccount: add_mailaccount requires a non-empty password")
+ }
+ resp, err := kaswrite.Call(ctx, cl.c, "mailaccount", addAction, AddParams(s))
+ if err != nil {
+ return "", err
+ }
+ return resp.Body.ReturnInfo.AsString(), nil
+}
+
+// AddParams builds the add_mailaccount KAS request parameter map. It is
+// the single source of truth for the request shape so the CLI dry-run
+// preview / audit record and the dispatched call cannot diverge. No
+// mail_login is sent — the server generates it. The address is sent
+// decomposed into local_part / domain_part (add_mailaccount's keys);
+// every optional field is sent verbatim, matching the captured request
+// fixture.
+func AddParams(s Spec) map[string]any {
+ return map[string]any{
+ "local_part": s.LocalPart,
+ "domain_part": s.DomainPart,
+ "mail_password": s.Password,
+ FieldWebmailAutologin: s.WebmailAutologin,
+ FieldResponder: s.Responder,
+ FieldResponderContentType: s.ResponderContentType,
+ FieldResponderDisplayName: s.ResponderDisplayName,
+ FieldResponderText: s.ResponderText,
+ FieldCopyAddress: s.CopyAddress,
+ FieldSenderAlias: s.SenderAlias,
+ FieldXListEnabled: s.XListEnabled,
+ FieldXListSent: s.XListSent,
+ FieldXListDrafts: s.XListDrafts,
+ FieldXListTrash: s.XListTrash,
+ FieldXListSpam: s.XListSpam,
+ FieldXListArchiv: s.XListArchiv,
+ FieldAllowNets: s.AllowNets,
+ }
+}
+
+// Update changes one or more mutable fields of an existing mail account
+// (update_mailaccount). fields holds only the keys the caller wants to
+// change (use the Field* constants; the password is FieldNewPassword
+// here, not the add-only mail_password); each is applied wholesale.
+// login and at least one field are required — update_mailaccount with
+// nothing to change is rejected before the SOAP call (the API would
+// fault nothing_to_do).
+func (cl *Client) Update(ctx context.Context, login string, fields map[string]string) error {
+ if login == "" {
+ return errors.New("mailaccount: update_mailaccount requires a non-empty mail login")
+ }
+ if len(fields) == 0 {
+ return errors.New("mailaccount: update_mailaccount requires at least one field to change")
+ }
+ _, err := kaswrite.Call(ctx, cl.c, "mailaccount", updateAction, UpdateParams(login, fields))
+ return err
+}
+
+// UpdateParams builds the update_mailaccount KAS request parameter map
+// (single source of truth, see AddParams): the mail_login identifier
+// plus every caller-supplied mutable field verbatim.
+func UpdateParams(login string, fields map[string]string) map[string]any {
+ params := map[string]any{FieldLogin: login}
+ for k, v := range fields {
+ params[k] = v
+ }
+ return params
+}
+
+// Delete removes a mail account (delete_mailaccount). A SOAP fault (e.g.
+// mail_login_not_found, in_progress, mail_loop_detected) is surfaced
+// verbatim by the Caller so the caller can classify it via the api error
+// helpers.
+func (cl *Client) Delete(ctx context.Context, login string) error {
+ if login == "" {
+ return errors.New("mailaccount: delete_mailaccount requires a non-empty mail login")
+ }
+ _, err := kaswrite.Call(ctx, cl.c, "mailaccount", deleteAction, DeleteParams(login))
+ return err
+}
+
+// DeleteParams builds the delete_mailaccount KAS request parameter map
+// (single source of truth, see AddParams).
+func DeleteParams(login string) map[string]any {
+ return map[string]any{FieldLogin: login}
+}
diff --git a/internal/mailaccount/write_test.go b/internal/mailaccount/write_test.go
new file mode 100644
index 0000000..875af53
--- /dev/null
+++ b/internal/mailaccount/write_test.go
@@ -0,0 +1,215 @@
+package mailaccount_test
+
+import (
+ "context"
+ "errors"
+ "strings"
+ "testing"
+
+ "github.com/chmmou/kasapi-cli/internal/mailaccount"
+ "github.com/chmmou/kasapi-cli/internal/soap"
+ "github.com/chmmou/kasapi-cli/internal/testutil"
+)
+
+func sampleSpec() mailaccount.Spec {
+ return mailaccount.Spec{
+ LocalPart: "info",
+ DomainPart: "example.com",
+ Password: "s3cret",
+ WebmailAutologin: "Y",
+ Responder: "N",
+ ResponderContentType: "text",
+ ResponderDisplayName: "Info",
+ XListEnabled: "Y",
+ XListSent: "Sent",
+ XListDrafts: "Drafts",
+ XListTrash: "Trash",
+ XListSpam: "Spam",
+ XListArchiv: "Archive",
+ }
+}
+
+func TestClientAdd(t *testing.T) {
+ t.Parallel()
+ resp := testutil.DecodeFixture(t, "mailaccount/add_mailaccount_response_success.xml")
+ fc := &testutil.FakeCaller{Resp: resp}
+ login, err := mailaccount.NewClient(fc).Add(context.Background(), sampleSpec())
+ if err != nil {
+ t.Fatalf("Add: %v", err)
+ }
+ if fc.GotAction != "add_mailaccount" {
+ t.Errorf("action = %q, want add_mailaccount", fc.GotAction)
+ }
+ wantParams := map[string]any{
+ "local_part": "info",
+ "domain_part": "example.com",
+ "mail_password": "s3cret",
+ "webmail_autologin": "Y",
+ "responder": "N",
+ "mail_responder_content_type": "text",
+ "mail_responder_displayname": "Info",
+ "mail_xlist_enabled": "Y",
+ "mail_xlist_sent": "Sent",
+ }
+ for k, v := range wantParams {
+ if fc.GotParams[k] != v {
+ t.Errorf("params[%q] = %v, want %v (full: %v)", k, fc.GotParams[k], v, fc.GotParams)
+ }
+ }
+ // add_mailaccount must not carry a mail_login (the server generates
+ // it), nor the update-only mail_new_password / is_active keys.
+ for _, k := range []string{"mail_login", "mail_new_password", "is_active"} {
+ if _, ok := fc.GotParams[k]; ok {
+ t.Errorf("add_mailaccount must not send %q: %v", k, fc.GotParams)
+ }
+ }
+ if login != "m0000001" {
+ t.Errorf("returned login = %q, want m0000001 (fixture ReturnInfo)", login)
+ }
+}
+
+func TestClientUpdate(t *testing.T) {
+ t.Parallel()
+ resp := testutil.DecodeFixture(t, "mailaccount/update_mailaccount_response_success.xml")
+ fc := &testutil.FakeCaller{Resp: resp}
+ fields := map[string]string{
+ mailaccount.FieldNewPassword: "n3wpass",
+ mailaccount.FieldIsActive: "Y",
+ mailaccount.FieldXListDrafts: "Entwürfe",
+ mailaccount.FieldResponder: "N",
+ }
+ if err := mailaccount.NewClient(fc).Update(context.Background(), "m0000001", fields); err != nil {
+ t.Fatalf("Update: %v", err)
+ }
+ if fc.GotAction != "update_mailaccount" {
+ t.Errorf("action = %q, want update_mailaccount", fc.GotAction)
+ }
+ wantParams := map[string]any{
+ "mail_login": "m0000001",
+ "mail_new_password": "n3wpass",
+ "is_active": "Y",
+ "mail_xlist_drafts": "Entwürfe",
+ "responder": "N",
+ }
+ for k, v := range wantParams {
+ if fc.GotParams[k] != v {
+ t.Errorf("params[%q] = %v, want %v (full: %v)", k, fc.GotParams[k], v, fc.GotParams)
+ }
+ }
+ // update_mailaccount uses mail_new_password — the add-only
+ // mail_password key must not leak through.
+ if _, ok := fc.GotParams["mail_password"]; ok {
+ t.Errorf("update_mailaccount must not send mail_password (it uses mail_new_password): %v", fc.GotParams)
+ }
+}
+
+func TestClientDelete(t *testing.T) {
+ t.Parallel()
+ resp := testutil.DecodeFixture(t, "mailaccount/delete_mailaccount_response_success.xml")
+ fc := &testutil.FakeCaller{Resp: resp}
+ if err := mailaccount.NewClient(fc).Delete(context.Background(), "m0000001"); err != nil {
+ t.Fatalf("Delete: %v", err)
+ }
+ if fc.GotAction != "delete_mailaccount" {
+ t.Errorf("action = %q, want delete_mailaccount", fc.GotAction)
+ }
+ if fc.GotParams["mail_login"] != "m0000001" {
+ t.Errorf("params = %v", fc.GotParams)
+ }
+}
+
+func TestWriteValidation(t *testing.T) {
+ t.Parallel()
+ c := mailaccount.NewClient(&testutil.FakeCaller{})
+ ctx := context.Background()
+
+ // Each missing required add field surfaces a per-field validation
+ // error (mentioning only that single field), not a combined message
+ // — the latter forces the caller to guess which field broke.
+ for _, tc := range []struct {
+ name string
+ mut func(*mailaccount.Spec)
+ wantSub string
+ }{
+ {"missing local part", func(s *mailaccount.Spec) { s.LocalPart = "" }, "local part"},
+ {"missing domain part", func(s *mailaccount.Spec) { s.DomainPart = "" }, "domain part"},
+ {"missing password", func(s *mailaccount.Spec) { s.Password = "" }, "password"},
+ } {
+ s := sampleSpec()
+ tc.mut(&s)
+ if _, err := c.Add(ctx, s); err == nil {
+ t.Errorf("Add %s: err = nil, want validation error", tc.name)
+ } else if !strings.Contains(err.Error(), tc.wantSub) {
+ t.Errorf("Add %s: err = %q, want it to mention %q", tc.name, err.Error(), tc.wantSub)
+ }
+ }
+ if err := c.Update(ctx, "", map[string]string{mailaccount.FieldIsActive: "Y"}); err == nil {
+ t.Error("Update empty login: err = nil, want validation error")
+ }
+ if err := c.Update(ctx, "m0000001", nil); err == nil {
+ t.Error("Update no fields: err = nil, want validation error")
+ }
+ if err := c.Delete(ctx, ""); err == nil {
+ t.Error("Delete empty login: err = nil, want validation error")
+ }
+}
+
+func TestUnexpectedReturnString(t *testing.T) {
+ t.Parallel()
+ resp := &soap.Response{Body: soap.ResponseBody{ReturnString: "FALSE"}}
+ c := mailaccount.NewClient(&testutil.FakeCaller{Resp: resp})
+ ctx := context.Background()
+ if _, err := c.Add(ctx, sampleSpec()); !errors.Is(err, mailaccount.ErrUnexpectedReturnString) {
+ t.Errorf("Add err = %v, want ErrUnexpectedReturnString", err)
+ }
+ if err := c.Update(ctx, "m0000001", map[string]string{mailaccount.FieldIsActive: "Y"}); !errors.Is(err, mailaccount.ErrUnexpectedReturnString) {
+ t.Errorf("Update err = %v, want ErrUnexpectedReturnString", err)
+ }
+ if err := c.Delete(ctx, "m0000001"); !errors.Is(err, mailaccount.ErrUnexpectedReturnString) {
+ t.Errorf("Delete err = %v, want ErrUnexpectedReturnString", err)
+ }
+}
+
+func TestWritePropagatesError(t *testing.T) {
+ t.Parallel()
+ want := errors.New("boom")
+ c := mailaccount.NewClient(&testutil.FakeCaller{Err: want})
+ ctx := context.Background()
+ if _, err := c.Add(ctx, sampleSpec()); !errors.Is(err, want) {
+ t.Errorf("Add err = %v, want %v", err, want)
+ }
+ if err := c.Update(ctx, "m0000001", map[string]string{mailaccount.FieldIsActive: "Y"}); !errors.Is(err, want) {
+ t.Errorf("Update err = %v, want %v", err, want)
+ }
+ if err := c.Delete(ctx, "m0000001"); !errors.Is(err, want) {
+ t.Errorf("Delete err = %v, want %v", err, want)
+ }
+}
+
+func TestParamBuilders(t *testing.T) {
+ t.Parallel()
+ add := mailaccount.AddParams(sampleSpec())
+ if add["local_part"] != "info" || add["domain_part"] != "example.com" || add["mail_password"] != "s3cret" {
+ t.Errorf("AddParams identity fields = %v", add)
+ }
+ // add must not contain the update-only keys.
+ for _, k := range []string{"mail_login", "mail_new_password", "is_active"} {
+ if _, ok := add[k]; ok {
+ t.Errorf("AddParams must not contain %q: %v", k, add)
+ }
+ }
+ upd := mailaccount.UpdateParams("m0000001", map[string]string{
+ mailaccount.FieldNewPassword: "n3wpass",
+ mailaccount.FieldIsActive: "N",
+ })
+ if upd["mail_login"] != "m0000001" || upd["mail_new_password"] != "n3wpass" || upd["is_active"] != "N" {
+ t.Errorf("UpdateParams = %v", upd)
+ }
+ if _, ok := upd["mail_password"]; ok {
+ t.Errorf("UpdateParams must not contain mail_password (update uses mail_new_password): %v", upd)
+ }
+ del := mailaccount.DeleteParams("m0000001")
+ if len(del) != 1 || del["mail_login"] != "m0000001" {
+ t.Errorf("DeleteParams = %v", del)
+ }
+}
diff --git a/testdata/mailaccount/add_mailaccount_request.xml b/testdata/mailaccount/add_mailaccount_request.xml
index 9d61b0c..b5fecc9 100644
--- a/testdata/mailaccount/add_mailaccount_request.xml
+++ b/testdata/mailaccount/add_mailaccount_request.xml
@@ -5,16 +5,17 @@
{
"KasRequestParams": {
- "local_part": "info",
- "domain_part": "example.com",
"mail_password": "REDACTED",
"webmail_autologin": "Y",
+ "local_part": "info",
+ "domain_part": "example.com",
"responder": "N",
"mail_responder_content_type": "text",
"mail_responder_displayname": "Info",
"responder_text": "",
"copy_adress": "",
"mail_sender_alias": "",
+ "mail_xlist_enabled": "Y",
"mail_xlist_sent": "Sent",
"mail_xlist_drafts": "Drafts",
"mail_xlist_trash": "Trash",
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_couldnt_get_kas_ressources.xml b/testdata/mailaccount/add_mailaccount_response_failed_couldnt_get_kas_ressources.xml
new file mode 100644
index 0000000..312c706
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_couldnt_get_kas_ressources.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ couldnt_get_kas_ressources
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml
new file mode 100644
index 0000000..cc065ac
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_archiv_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml
new file mode 100644
index 0000000..6517490
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_drafts_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_duplicate_folder.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_duplicate_folder.xml
new file mode 100644
index 0000000..a732e9a
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_duplicate_folder.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_duplicate_folder
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml
new file mode 100644
index 0000000..0194a5c
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_enabled_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml
new file mode 100644
index 0000000..0568ecd
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_sent_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml
new file mode 100644
index 0000000..eddbfdc
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_spam_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml
new file mode 100644
index 0000000..789bea5
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_trash_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml
new file mode 100644
index 0000000..dbcbe57
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_contentype_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml b/testdata/mailaccount/add_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml
new file mode 100644
index 0000000..eb4c672
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_displayname_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml b/testdata/mailaccount/add_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml
new file mode 100644
index 0000000..b635b85
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_not_allowed_for_catchall_adresses
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_responder_startdate_gt_enddate.xml b/testdata/mailaccount/add_mailaccount_response_failed_responder_startdate_gt_enddate.xml
new file mode 100644
index 0000000..5a30cf5
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_responder_startdate_gt_enddate.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_startdate_gt_enddate
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_failed_sender_alias_domain_in_kas.xml b/testdata/mailaccount/add_mailaccount_response_failed_sender_alias_domain_in_kas.xml
new file mode 100644
index 0000000..0aae5af
--- /dev/null
+++ b/testdata/mailaccount/add_mailaccount_response_failed_sender_alias_domain_in_kas.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ sender_alias_domain_in_kas
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/add_mailaccount_response_success.xml b/testdata/mailaccount/add_mailaccount_response_success.xml
index a0e976f..964a8f7 100644
--- a/testdata/mailaccount/add_mailaccount_response_success.xml
+++ b/testdata/mailaccount/add_mailaccount_response_success.xml
@@ -8,7 +8,7 @@
-
KasRequestTime
- 1777736000
+ 1779406589
-
KasRequestType
@@ -17,6 +17,14 @@
-
KasRequestParams
+
-
+ mail_password
+ REDACTED
+
+ -
+ webmail_autologin
+ Y
+
-
local_part
info
@@ -25,10 +33,6 @@
domain_part
example.com
- -
- webmail_autologin
- Y
-
-
responder
N
@@ -41,6 +45,10 @@
mail_responder_displayname
Info
+ -
+ responder_text
+
+
-
copy_adress
@@ -49,6 +57,10 @@
mail_sender_alias
+ -
+ mail_xlist_enabled
+ Y
+
-
mail_xlist_sent
Sent
@@ -102,7 +114,7 @@
-
text
- The mail account has been created. The automatically generated login is: m0000001
+ The email account info@example.com is going to be created. The username <i>m0000001</i> has been generated automatically.
@@ -112,4 +124,4 @@
-
+
\ No newline at end of file
diff --git a/testdata/mailaccount/delete_mailaccount_response_success.xml b/testdata/mailaccount/delete_mailaccount_response_success.xml
index 4a674c2..60ce2f3 100644
--- a/testdata/mailaccount/delete_mailaccount_response_success.xml
+++ b/testdata/mailaccount/delete_mailaccount_response_success.xml
@@ -38,7 +38,7 @@
-
ReturnInfo
-
+ m0000001
-
Msg
@@ -50,7 +50,7 @@
-
text
- The mail account was deleted.
+ The mail account has been deleted.
@@ -60,4 +60,4 @@
-
+
\ No newline at end of file
diff --git a/testdata/mailaccount/get_mailaccount_response_success.xml b/testdata/mailaccount/get_mailaccount_response_success.xml
index f489f9c..855beba 100644
--- a/testdata/mailaccount/get_mailaccount_response_success.xml
+++ b/testdata/mailaccount/get_mailaccount_response_success.xml
@@ -50,11 +50,11 @@
-
mail_adresses
- m0000001@example.com
+ info@example.com
-
mail_addresses
- m0000001@example.com
+ info@example.com
-
mail_comment
@@ -70,7 +70,7 @@
-
mail_responder_displayname
-
+ Info
-
mail_responder_content_type
@@ -90,11 +90,11 @@
-
mail_spamfilter
- pdw,sf
+
-
in_progress
- FALSE
+ TRUE
-
mail_xlist_enabled
@@ -102,15 +102,15 @@
-
mail_xlist_sent
- Gesendet
+ Sent
-
mail_xlist_drafts
- Entwürfe
+ Drafts
-
mail_xlist_trash
- Papierkorb
+ Trash
-
mail_xlist_spam
@@ -118,11 +118,11 @@
-
mail_xlist_archiv
- Archiv
+ Archive
-
used_mailaccount_space
- 212.200195312
+ 0
-
mail_is_active
diff --git a/testdata/mailaccount/get_mailaccounts_response_success.xml b/testdata/mailaccount/get_mailaccounts_response_success.xml
index 7361a23..b1c17a9 100644
--- a/testdata/mailaccount/get_mailaccounts_response_success.xml
+++ b/testdata/mailaccount/get_mailaccounts_response_success.xml
@@ -45,11 +45,11 @@
-
mail_adresses
- m0000001@example.com
+ info@example.com
-
mail_addresses
- m0000001@example.com
+ info@example.com
-
mail_comment
diff --git a/testdata/mailaccount/update_mailaccount_request.xml b/testdata/mailaccount/update_mailaccount_request.xml
index 466b917..3a4d5e6 100644
--- a/testdata/mailaccount/update_mailaccount_request.xml
+++ b/testdata/mailaccount/update_mailaccount_request.xml
@@ -9,10 +9,19 @@
"mail_new_password": "REDACTED",
"webmail_autologin": "Y",
"responder": "N",
+ "mail_responder_content_type": "html",
+ "mail_responder_displayname": "Info Update",
"responder_text": "",
"copy_adress": "",
"is_active": "Y",
- "mail_sender_alias": ""
+ "mail_sender_alias": "",
+ "mail_xlist_enabled": "Y",
+ "mail_xlist_sent": "gesendete Objekte",
+ "mail_xlist_drafts": "Entwürfe",
+ "mail_xlist_trash": "Papierkorb",
+ "mail_xlist_spam": "Spam",
+ "mail_xlist_archiv": "Archiv",
+ "mail_allow_nets": ""
},
"kas_action": "update_mailaccount",
"kas_auth_data": "REDACTED",
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_email_domain_doesnt_exist.xml b/testdata/mailaccount/update_mailaccount_response_failed_email_domain_doesnt_exist.xml
new file mode 100644
index 0000000..ddf3ede
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_email_domain_doesnt_exist.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ email_domain_doesnt_exist
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_is_active_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_is_active_syntax_incorrect.xml
new file mode 100644
index 0000000..bc1e7a3
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_is_active_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ is_active_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml
new file mode 100644
index 0000000..cc065ac
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_archiv_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_archiv_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml
new file mode 100644
index 0000000..6517490
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_drafts_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_drafts_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_duplicate_folder.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_duplicate_folder.xml
new file mode 100644
index 0000000..a732e9a
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_duplicate_folder.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_duplicate_folder
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml
new file mode 100644
index 0000000..0194a5c
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_enabled_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_enabled_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml
new file mode 100644
index 0000000..0568ecd
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_sent_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_sent_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml
new file mode 100644
index 0000000..eddbfdc
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_spam_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_spam_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml
new file mode 100644
index 0000000..789bea5
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_mail_xlist_trash_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ mail_xlist_trash_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml
new file mode 100644
index 0000000..dbcbe57
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_responder_contentype_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_contentype_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml b/testdata/mailaccount/update_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml
new file mode 100644
index 0000000..eb4c672
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_responder_displayname_syntax_incorrect.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_displayname_syntax_incorrect
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml b/testdata/mailaccount/update_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml
new file mode 100644
index 0000000..b635b85
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_responder_not_allowed_for_catchall_adresses.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_not_allowed_for_catchall_adresses
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_responder_startdate_gt_enddate.xml b/testdata/mailaccount/update_mailaccount_response_failed_responder_startdate_gt_enddate.xml
new file mode 100644
index 0000000..5a30cf5
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_responder_startdate_gt_enddate.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ responder_startdate_gt_enddate
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_failed_sender_alias_domain_in_kas.xml b/testdata/mailaccount/update_mailaccount_response_failed_sender_alias_domain_in_kas.xml
new file mode 100644
index 0000000..0aae5af
--- /dev/null
+++ b/testdata/mailaccount/update_mailaccount_response_failed_sender_alias_domain_in_kas.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ SOAP-ENV:Server
+ sender_alias_domain_in_kas
+ KasApi
+
+
+
+
diff --git a/testdata/mailaccount/update_mailaccount_response_success.xml b/testdata/mailaccount/update_mailaccount_response_success.xml
index 33f413e..710e761 100644
--- a/testdata/mailaccount/update_mailaccount_response_success.xml
+++ b/testdata/mailaccount/update_mailaccount_response_success.xml
@@ -8,7 +8,7 @@
-
KasRequestTime
- 1777736120
+ 1779410980
-
KasRequestType
@@ -21,6 +21,10 @@
mail_login
m0000001
+ -
+ mail_new_password
+ REDACTED
+
-
webmail_autologin
Y
@@ -29,6 +33,18 @@
responder
N
+ -
+ mail_responder_content_type
+ html
+
+ -
+ mail_responder_displayname
+ Info Update
+
+ -
+ responder_text
+
+
-
copy_adress
@@ -41,6 +57,34 @@
mail_sender_alias
+ -
+ mail_xlist_enabled
+ Y
+
+ -
+ mail_xlist_sent
+ gesendete Objekte
+
+ -
+ mail_xlist_drafts
+ Entwürfe
+
+ -
+ mail_xlist_trash
+ Papierkorb
+
+ -
+ mail_xlist_spam
+ Spam
+
+ -
+ mail_xlist_archiv
+ Archiv
+
+ -
+ mail_allow_nets
+
+
@@ -58,7 +102,7 @@
-
ReturnInfo
-
+ m0000001
-
Msg
@@ -70,7 +114,7 @@
-
text
- The mail account m0000001 has been edited.
+ The mailbox of the address info@example.com (login m0000001) has been processed.
@@ -80,4 +124,4 @@
-
+
\ No newline at end of file