diff --git a/.github/workflows/fastmail-jmap-tests.yml b/.github/workflows/fastmail-jmap-tests.yml index d74ec5c9..476d4069 100644 --- a/.github/workflows/fastmail-jmap-tests.yml +++ b/.github/workflows/fastmail-jmap-tests.yml @@ -13,5 +13,5 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: cargo test --no-default-features --features jmap,rustls-ring --test fastmail-jmap -- --ignored env: - EMAIL: ${{ secrets.FASTMAIL_EMAIL }} - BEARER_TOKEN: ${{ secrets.FASTMAIL_BEARER_TOKEN }} + FASTMAIL_EMAIL: ${{ secrets.FASTMAIL_EMAIL }} + FASTMAIL_BEARER_TOKEN: ${{ secrets.FASTMAIL_BEARER_TOKEN }} diff --git a/.github/workflows/stalwart-jmap-tests.yml b/.github/workflows/stalwart-jmap-tests.yml index b91096c2..b902107c 100644 --- a/.github/workflows/stalwart-jmap-tests.yml +++ b/.github/workflows/stalwart-jmap-tests.yml @@ -16,10 +16,16 @@ jobs: - run: | echo "ADMIN_PASSWORD=$(docker logs stalwart-test-for-himalaya 2>&1 | grep -oP "(?<=with password ')[^']+")" >> $GITHUB_ENV - run: | - curl -u "admin:${ADMIN_PASSWORD}" -X POST -H 'Content-Type: application/json' -d '{"type":"domain","name":"pimalaya.org","description":"","quota":0,"secrets":[],"emails":[],"urls":[],"memberOf":[],"roles":[],"lists":[],"members":[],"enabledPermissions":[],"disabledPermissions":[],"externalMembers":[]}' http://localhost:8080/api/principal + curl -X POST \ + -u "admin:${ADMIN_PASSWORD}" \ + -H 'Content-Type: application/json' \ + -d '{"type":"domain","name":"pimalaya.org"}' \ + http://localhost:8080/api/principal - run: | - curl -u "admin:${ADMIN_PASSWORD}" -X POST -H 'Content-Type: application/json' -d '{"type":"individual","name":"test","description":"","quota":0,"secrets":["test"],"emails":["test@pimalaya.org"],"memberOf":[],"roles":["user"],"lists":[],"enabledPermissions":[],"disabledPermissions":[],"externalMembers":[]}' http://localhost:8080/api/principal + curl -X POST \ + -u "admin:${ADMIN_PASSWORD}" \ + -H 'Content-Type: application/json' \ + -d '{"type":"individual","name":"test","emails":["test@pimalaya.org"],"secrets":["test"],"roles":["user"]}' \ + http://localhost:8080/api/principal - run: cargo test --no-default-features --features jmap,rustls-ring --test stalwart-jmap -- --ignored - env: - EMAIL: test@pimalaya.org - run: docker stop stalwart-test-for-himalaya diff --git a/src/jmap/email/copy.rs b/src/jmap/email/copy.rs index 5869c58d..98fed699 100644 --- a/src/jmap/email/copy.rs +++ b/src/jmap/email/copy.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ rfc8621::coroutines::email_copy::{JmapEmailCopy, JmapEmailCopyResult}, @@ -67,20 +67,25 @@ impl JmapEmailCopyCommand { }; if !not_created.is_empty() { - let mut ctx = anyhow!("Copy JMAP email(s) error"); + let mut msg = String::from("Copy JMAP email(s) error"); for (id, err) in not_created { - if let Some(desc) = &err.description { - ctx = anyhow!("{id}: {desc}").context(ctx); - } + msg.push_str(&format!("\n `{id}`")); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("{id}: Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); + } + + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); } } - bail!(ctx) + bail!(msg) } printer.out(Message::new("Email(s) successfully copied")) diff --git a/src/jmap/email/delete.rs b/src/jmap/email/delete.rs index 8f0c019c..ffc0e5ca 100644 --- a/src/jmap/email/delete.rs +++ b/src/jmap/email/delete.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::rfc8621::coroutines::email_set::{JmapEmailSet, JmapEmailSetArgs, JmapEmailSetResult}; use io_stream::runtimes::std::handle; @@ -36,20 +36,25 @@ impl JmapEmailDestroyCommand { }; if !not_destroyed.is_empty() { - let mut ctx = anyhow!("Destroy JMAP email(s) error"); + let mut msg = String::from("Destroy JMAP email(s) error"); for (id, err) in not_destroyed { - if let Some(desc) = &err.description { - ctx = anyhow!("{id}: {desc}").context(ctx); - } + msg.push_str(&format!("\n `{id}`")); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("{id}: Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); + } + + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); } } - bail!(ctx) + bail!(msg) } printer.out(Message::new("Email(s) successfully deleted")) diff --git a/src/jmap/email/import.rs b/src/jmap/email/import.rs index de14257c..e2aaadbc 100644 --- a/src/jmap/email/import.rs +++ b/src/jmap/email/import.rs @@ -3,7 +3,7 @@ use std::{ io::{stdin, BufRead, IsTerminal}, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ rfc8620::coroutines::blob_upload::{JmapBlobUpload, JmapBlobUploadResult}, @@ -115,7 +115,7 @@ impl ImportEmailCommand { let mut coroutine = JmapEmailImport::new(&jmap.session, &jmap.http_auth, emails)?; let mut arg = None; - let errs = loop { + let not_created = loop { match coroutine.resume(arg.take()) { JmapEmailImportResult::Io { io } => arg = Some(handle(&mut jmap.stream, io)?), JmapEmailImportResult::Ok { not_created, .. } => break not_created, @@ -123,19 +123,22 @@ impl ImportEmailCommand { } }; - if let Some(err) = errs.get(&blob_id) { - let mut ctx = anyhow!("Import JMAP email from blob `{blob_id}` error"); - - if let Some(desc) = &err.description { - ctx = anyhow!("{desc}").context(ctx); - } + if let Some(err) = not_created.get(&blob_id) { + let mut msg = format!("Import JMAP email from blob `{blob_id}` error"); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); } - bail!(ctx); + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); + } + + bail!(msg); } printer.out(Message::new("Email successfully imported")) diff --git a/src/jmap/email/update.rs b/src/jmap/email/update.rs index c2fd4d76..065af0ee 100644 --- a/src/jmap/email/update.rs +++ b/src/jmap/email/update.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::rfc8621::coroutines::email_set::{JmapEmailSet, JmapEmailSetArgs, JmapEmailSetResult}; use io_stream::runtimes::std::handle; @@ -85,20 +85,25 @@ impl JmapEmailUpdateCommand { }; if !not_updated.is_empty() { - let mut ctx = anyhow!("Update JMAP email(s) error"); + let mut msg = String::from("Update JMAP email(s) error"); for (id, err) in not_updated { - if let Some(desc) = &err.description { - ctx = anyhow!("{id}: {desc}").context(ctx); - } + msg.push_str(&format!("\n `{id}`")); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("{id}: Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); + } + + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); } } - bail!(ctx) + bail!(msg) } printer.out(Message::new("Email(s) successfully updated")) diff --git a/src/jmap/identity/delete.rs b/src/jmap/identity/delete.rs index 3ecf53ba..55f8fb46 100644 --- a/src/jmap/identity/delete.rs +++ b/src/jmap/identity/delete.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::rfc8621::coroutines::identity_set::{ JmapIdentitySet, JmapIdentitySetArgs, JmapIdentitySetResult, @@ -38,20 +38,25 @@ impl DeleteIdentityCommand { }; if !not_destroyed.is_empty() { - let mut ctx = anyhow!("Destroy JMAP identities error"); + let mut msg = String::from("Destroy JMAP identities error"); for (id, err) in not_destroyed { - if let Some(desc) = &err.description { - ctx = anyhow!("{id}: {desc}").context(ctx); - } + msg.push_str(&format!("\n `{id}`")); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("{id}: Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); + } + + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); } } - bail!(ctx) + bail!(msg) } printer.out(Message::new("Identity successfully deleted")) diff --git a/src/jmap/identity/update.rs b/src/jmap/identity/update.rs index 180dc4e6..d1bbc6cb 100644 --- a/src/jmap/identity/update.rs +++ b/src/jmap/identity/update.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ rfc8621::coroutines::identity_set::{ @@ -48,7 +48,7 @@ impl UpdateIdentityCommand { let mut coroutine = JmapIdentitySet::new(&jmap.session, &jmap.http_auth, args)?; let mut arg = None; - let errs = loop { + let not_updated = loop { match coroutine.resume(arg.take()) { JmapIdentitySetResult::Io { io } => arg = Some(handle(&mut jmap.stream, io)?), JmapIdentitySetResult::Ok { not_updated, .. } => break not_updated, @@ -56,19 +56,22 @@ impl UpdateIdentityCommand { } }; - if let Some(err) = errs.get(&self.id) { - let mut ctx = anyhow!("Update identity `{}` error", &self.id); - - if let Some(desc) = &err.description { - ctx = anyhow!("{desc}").context(ctx); - } + if let Some(err) = not_updated.get(&self.id) { + let mut msg = format!("Update identity `{}` error", self.id); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); } - bail!(ctx); + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); + } + + bail!(msg); } printer.out(Message::new("Identity successfully updated")) diff --git a/src/jmap/mailbox/create.rs b/src/jmap/mailbox/create.rs index e2ed4f0b..f0ca0e67 100644 --- a/src/jmap/mailbox/create.rs +++ b/src/jmap/mailbox/create.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ rfc8621::coroutines::mailbox_set::{JmapMailboxSet, JmapMailboxSetArgs, JmapMailboxSetResult}, @@ -56,21 +56,22 @@ impl JmapMailboxCreateCommand { } }; - if !not_created.is_empty() { - let mut ctx = anyhow!("Create JMAP mailbox `{}` error", self.name); + if let Some(err) = not_created.get(&self.name) { + let mut msg = format!("Create JMAP mailbox `{}` error", self.name); - for (_, err) in not_created { - if let Some(desc) = &err.description { - ctx = anyhow!(desc.clone()).context(ctx); - } - - if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("Invalid properties {props}").context(ctx); - } + if !err.properties.is_empty() { + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); } - bail!(ctx) + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); + } + + bail!(msg) } printer.out(Message::new("Mailbox successfully created")) diff --git a/src/jmap/mailbox/destroy.rs b/src/jmap/mailbox/destroy.rs index f0498c3a..7acfe568 100644 --- a/src/jmap/mailbox/destroy.rs +++ b/src/jmap/mailbox/destroy.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::rfc8621::coroutines::mailbox_set::{ JmapMailboxSet, JmapMailboxSetArgs, JmapMailboxSetResult, @@ -40,20 +40,25 @@ impl JmapMailboxDestroyCommand { }; if !not_destroyed.is_empty() { - let mut ctx = anyhow!("Destroy JMAP mailbox(es) error"); + let mut msg = String::from("Destroy JMAP mailbox(es) error"); for (id, err) in not_destroyed { - if let Some(desc) = &err.description { - ctx = anyhow!("{id}: {desc}").context(ctx); - } + msg.push_str(&format!("\n `{id}`")); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("{id}: Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); + } + + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); } } - bail!(ctx) + bail!(msg) } printer.out(Message::new("Mailbox successfully deleted")) diff --git a/src/jmap/mailbox/update.rs b/src/jmap/mailbox/update.rs index 45c20465..00d1436f 100644 --- a/src/jmap/mailbox/update.rs +++ b/src/jmap/mailbox/update.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ rfc8621::coroutines::mailbox_set::{JmapMailboxSet, JmapMailboxSetArgs, JmapMailboxSetResult}, @@ -72,7 +72,7 @@ impl JmapMailboxUpdateCommand { let mut arg = None; let mut coroutine = JmapMailboxSet::new(&jmap.session, &jmap.http_auth, args)?; - let errs = loop { + let not_updated = loop { match coroutine.resume(arg.take()) { JmapMailboxSetResult::Io { io } => arg = Some(handle(&mut jmap.stream, io)?), JmapMailboxSetResult::Ok { not_updated, .. } => break not_updated, @@ -80,19 +80,22 @@ impl JmapMailboxUpdateCommand { } }; - if let Some(err) = errs.get(&self.id) { - let mut ctx = anyhow!("Update JMAP mailbox `{}` error", self.id); - - if let Some(desc) = &err.description { - ctx = anyhow!("{desc}").context(ctx); - } + if let Some(err) = not_updated.get(&self.id) { + let mut msg = format!("Update JMAP mailbox `{}` error", self.id); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); } - bail!(ctx); + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); + } + + bail!(msg); } printer.out(Message::new("Mailbox successfully updated")) diff --git a/src/jmap/submission/cancel.rs b/src/jmap/submission/cancel.rs index f3fab71c..9de57333 100644 --- a/src/jmap/submission/cancel.rs +++ b/src/jmap/submission/cancel.rs @@ -38,16 +38,26 @@ impl CancelSubmissionCommand { } }; - for (id, err) in ¬_updated { - let mut ctx = anyhow!("Cancel submission `{id}` error"); - if let Some(desc) = &err.description { - ctx = anyhow!("{desc}").context(ctx); + if !not_updated.is_empty() { + let mut msg = String::from("Cancel submission(s) error"); + + for (id, err) in ¬_updated { + msg.push_str(&format!("\n `{id}`")); + + if !err.properties.is_empty() { + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); + } + + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); + } } - if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("Invalid properties {props}").context(ctx); - } - bail!(ctx); + + bail!(msg); } printer.out(Message::new(format!( diff --git a/src/jmap/submission/create.rs b/src/jmap/submission/create.rs index d20f2a2c..a725c803 100644 --- a/src/jmap/submission/create.rs +++ b/src/jmap/submission/create.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ rfc8621::coroutines::email_submission_set::{ @@ -75,7 +75,7 @@ impl CreateSubmissionCommand { JmapEmailSubmissionSet::new(&jmap.session, &jmap.http_auth, submissions)?; let mut arg = None; - let (created, errs) = loop { + let (created, not_created) = loop { match coroutine.resume(arg.take()) { JmapEmailSubmissionSetResult::Io { io } => { arg = Some(handle(&mut jmap.stream, io)?) @@ -89,19 +89,22 @@ impl CreateSubmissionCommand { } }; - if let Some(err) = errs.get(&self.email_id) { - let mut ctx = anyhow!("Send email `{}` error", &self.email_id); - - if let Some(desc) = &err.description { - ctx = anyhow!("{desc}").context(ctx); - } + if let Some(err) = not_created.get(&self.email_id) { + let mut msg = format!("Send email `{}` error", self.email_id); if !err.properties.is_empty() { - let props = err.properties.join(", "); - ctx = anyhow!("Invalid properties {props}").context(ctx); + msg.push_str(": invalid properties `"); + msg.push_str(&err.properties.join("`, `")); + msg.push('`'); } - bail!(ctx); + if let Some(desc) = &err.description { + msg.push_str(" ("); + msg.push_str(desc.to_lowercase().trim_end_matches(['.', '\n'])); + msg.push(')'); + } + + bail!(msg); } let table = SubmissionsTable { diff --git a/tests/common/jmap.rs b/tests/common/jmap.rs index 1e5b1cb6..323ebd9a 100644 --- a/tests/common/jmap.rs +++ b/tests/common/jmap.rs @@ -72,9 +72,7 @@ fn parse_output(config: &Path, args: &[&str]) -> T { /// /// Exercises every command in a single ordered flow. Pass a path to a /// valid TOML config file with a default JMAP account configured. -pub fn run(config: &Path) { - let email = env::var("EMAIL").expect("EMAIL env var"); - +pub fn run(config: &Path, email: String) { let ts = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() diff --git a/tests/fastmail-jmap.rs b/tests/fastmail-jmap.rs index 394798d9..46c8c967 100644 --- a/tests/fastmail-jmap.rs +++ b/tests/fastmail-jmap.rs @@ -6,9 +6,10 @@ use std::{env, io::Write}; use tempfile::NamedTempFile; #[test] -#[ignore = "requires BEARER_TOKEN env var and --ignored"] +#[ignore = "requires FASTMAIL_{EMAIL,BEARER_TOKEN} env vars and --ignored"] fn fastmail_jmap() { - let token = env::var("BEARER_TOKEN").expect("BEARER_TOKEN env var"); + let email = env::var("FASTMAIL_EMAIL").expect("FASTMAIL_EMAIL env var"); + let token = env::var("FASTMAIL_BEARER_TOKEN").expect("FASTMAIL_BEARER_TOKEN env var"); let mut config = NamedTempFile::new().unwrap(); let config_tpl = format!( @@ -20,5 +21,5 @@ jmap.auth.bearer.token.raw = "{token}""# config.write(&config_tpl.into_bytes()).unwrap(); - jmap::run(config.path()); + jmap::run(config.path(), email); } diff --git a/tests/stalwart-jmap.rs b/tests/stalwart-jmap.rs index 380f294c..007ec3e1 100644 --- a/tests/stalwart-jmap.rs +++ b/tests/stalwart-jmap.rs @@ -6,14 +6,14 @@ use std::{env, io::Write}; use tempfile::NamedTempFile; #[test] -#[ignore = "requires URL, USER, PASS env vars and --ignored"] +#[ignore = "requires STALWART_{EMAIL,URL,USER,PASS} env vars and --ignored"] fn stalwart_jmap() { + let email = env::var("STALWART_EMAIL").unwrap_or("test@pimalaya.org".into()); + let url = env::var("STALWART_URL").unwrap_or("http://localhost:8080/jmap/session".into()); + let user = env::var("STALWART_USER").unwrap_or("test".into()); + let pass = env::var("STALWART_PASS").unwrap_or("test".into()); + let mut config = NamedTempFile::new().unwrap(); - - let url = env::var("URL").unwrap_or("http://localhost:8080/jmap/session".into()); - let user = env::var("USER").unwrap_or("test".into()); - let pass = env::var("PASS").unwrap_or("test".into()); - let config_tpl = format!( r#"[accounts.stalwart] default = true @@ -24,5 +24,5 @@ jmap.auth.basic.password.raw = "{pass}""# config.write(&config_tpl.into_bytes()).unwrap(); - jmap::run(config.path()); + jmap::run(config.path(), email); } diff --git a/tests/stalwart.sh b/tests/stalwart.sh deleted file mode 100755 index 39aa3dbd..00000000 --- a/tests/stalwart.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e - -docker run -d --rm --name stalwart-test-for-himalaya -p 8080:8080 stalwartlabs/stalwart:latest-alpine - -sleep 1 - -admin_password=$(docker logs stalwart-test-for-himalaya 2>&1 | grep -oP "(?<=with password ')[^']+") - -curl -u "admin:${admin_password}" -X POST -H 'Content-Type: application/json' -d '{"type":"domain","name":"pimalaya.org","description":"","quota":0,"secrets":[],"emails":[],"urls":[],"memberOf":[],"roles":[],"lists":[],"members":[],"enabledPermissions":[],"disabledPermissions":[],"externalMembers":[]}' http://localhost:8080/api/principal - -curl -u "admin:${admin_password}" -X POST -H 'Content-Type: application/json' -d '{"type":"individual","name":"test","description":"","quota":0,"secrets":["test"],"emails":["test@pimalaya.org"],"memberOf":[],"roles":["user"],"lists":[],"enabledPermissions":[],"disabledPermissions":[],"externalMembers":[]}' http://localhost:8080/api/principal