@openemail/cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/openemail.js +165 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -126,7 +126,7 @@ openemail temp read
126
126
  openemail temp delete --yes
127
127
  ```
128
128
 
129
- A disposable inbox needs no account and no key. `temp new` prints the address, and the CLI keeps the inbox's token so the other `temp` commands find it by its id or address. `temp watch --first` waits for the first message, which suits a script waiting for a sign-up code.
129
+ A disposable inbox needs no account and no key. `temp new` prints the address, and the CLI keeps the inbox's token so the other `temp` commands find it by its id or address. `temp watch --first` waits for the first message, which suits a script waiting for a sign-up code. `temp-mail extend` adds up to an hour, within 24 hours of the inbox being created, and the CLI keeps the new token it returns. `temp delete` moves the mail to the bin and forgets the token here, while the lease runs on until it expires.
130
130
 
131
131
  ### Every resource
132
132
  ```bash
package/openemail.js CHANGED
@@ -344,7 +344,8 @@ const CLI_CALLBACK = {
344
344
  APP_PATH: '/cli/callback',
345
345
  LOOPBACK_PATH: '/callback',
346
346
  LOOPBACK_HOST: '127.0.0.1',
347
- PASTE_SEPARATOR: '.'};
347
+ PASTE_SEPARATOR: '.'
348
+ };
348
349
  const OAUTH_PARAMS = {
349
350
  RESPONSE_TYPE: 'response_type',
350
351
  CLIENT_ID: 'client_id',
@@ -672,7 +673,7 @@ const AUTH_MESSAGES = {
672
673
  : `OpenEmail could not refresh your sign-in (status ${status}). Try again shortly.`,
673
674
  SESSION_EXPIRED_PROFILE_NEXT: (name) => `Run \`openemail login --profile ${name}\` again.`,
674
675
  SERVER_SAID: (reason) => `The server said: ${reason}`,
675
- CLIENT_NAME: (host) => `OpenEmail CLI on ${host}`,
676
+ CLIENT_NAME: (host) => `${OAUTH_CLIENT.NAME_PREFIX}${host}`,
676
677
  WHO_WORKSPACE: (workspace) => `workspace ${workspace}`,
677
678
  WHO_KEY: (keyId) => `key ${keyId}`,
678
679
  KEY_STAYS_VALID: (keyId) => `The key itself still works. Revoke it with \`openemail keys revoke ${keyId}\` if you no longer need it.`,
@@ -818,7 +819,6 @@ const DOCS_CHAT = {
818
819
  };
819
820
  const DOCS_PATHS = {
820
821
  DOCS: '/docs',
821
- DOCS_PREFIX: 'docs/',
822
822
  MARKDOWN_EXTENSION: '.md',
823
823
  INDEX: '/llms.txt',
824
824
  ANCHOR: '#',
@@ -829,7 +829,8 @@ const DOCS_PATTERNS = {
829
829
  LEADING_SLASHES: /^\/+/,
830
830
  TRAILING_SLASHES: /\/+$/,
831
831
  LINE_BREAK: /\r?\n/,
832
- DOT_SEGMENT: /^\.{1,2}$/
832
+ DOT_SEGMENT: /^\.{1,2}$/,
833
+ DOCS_PREFIX: /^(?:[a-z]{2}\/)?docs(?:\/|$)/i
833
834
  };
834
835
  const DOCS_FETCH = {
835
836
  ACCEPT: 'text/markdown, text/plain;q=0.9',
@@ -1111,7 +1112,7 @@ const BOOLEAN_TRUE_WORDS = new Set(['true', '1', 'yes', 'on']);
1111
1112
  const BOOLEAN_FALSE_WORDS = new Set(['false', '0', 'no', 'off']);
1112
1113
  const NEGATIVE_NUMBER_PATTERN = /^-\d+(\.\d+)?$/;
1113
1114
 
1114
- var version$1 = "0.0.1";
1115
+ var version$1 = "0.0.3";
1115
1116
 
1116
1117
  const VERSION$1 = version$1;
1117
1118
  const CLI_NAME = 'openemail';
@@ -1244,7 +1245,7 @@ const SCRIPTING_ROWS = [
1244
1245
  const HELP_JSON_SCHEMA_VERSION = 1;
1245
1246
  const CLI_NAME_WORD_PATTERN = new RegExp(`(^|[^\\w-])${CLI_NAME}($|[^\\w-])`);
1246
1247
 
1247
- var version = "0.0.5";
1248
+ var version = "0.0.7";
1248
1249
 
1249
1250
  const BUILD_BASE_URL = 'https://api.openemail.uk';
1250
1251
 
@@ -3495,7 +3496,7 @@ const DomainsCreateAddressMethod = {
3495
3496
  scopes: ['domains:write'],
3496
3497
  parameters: [
3497
3498
  { name: 'id', type: 'string', required: true, description: 'Domain id from `list` or `create`, a UUID.' },
3498
- { name: 'body.localPart', type: 'string', required: true, description: 'The part in front of the @, 1 to 64 characters once trimmed, lower cased. Letters, digits and ! # $ % & \' * + / = ? ^ _ ` { | } ~ - are allowed, and so are dots between them. `*` on its own is how the catch-all is written and is refused.' },
3499
+ { name: 'body.localPart', type: 'string', required: true, description: 'The part in front of the @, 1 to 64 characters once trimmed, lower cased. Letters, digits, the backtick and ! # $ % & \' * + / = ? ^ _ { | } ~ - are allowed, and so are dots between them. `*` on its own is how the catch-all is written and is refused.' },
3499
3500
  { name: 'body.label', type: 'string | null', required: false, description: 'A name for the address shown in the app, trimmed, up to 120 characters. Leave it out, or send null or an empty string, for none.' },
3500
3501
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
3501
3502
  { name: 'options.apiKey', type: 'string', required: false, description: 'Overrides the client\'s API key for this call only.' }
@@ -7500,14 +7501,14 @@ const TempMailCreateMethod = {
7500
7501
  slug: 'temp-mail/create',
7501
7502
  signature: 'create(body?: TempInboxCreate, options?: InboxScope): Promise<CreatedTempInboxResource>',
7502
7503
  summary: 'Create a disposable inbox and its access token',
7503
- description: 'Mints a disposable address and resolves with the inbox plus its `token`. It needs no credential, and this is the only response that ever carries the token. The server keeps only an HMAC of it, so a lost token cannot be recovered, and every other method except `listDomains` needs it. Store it before you show the address to anyone.\n\nThe body is optional and so is every field in it. With nothing, you get a 12 character generated local part on a domain picked at random from the pool, leased for 60 minutes. A chosen `localPart` is lowercased and must be 3 to 32 letters, digits, dots, dashes or underscores, starting and ending with a letter or digit, or it is 422 `invalid_address`. Names such as `postmaster`, `abuse` and `support` are 422 `reserved_address`, and an address the domain owner created as a real mailbox is 409 `address_taken`.\n\nMinting is capped per client IP at 6 inboxes an hour and 30 a day, so a shared network shares the ceiling, and going past it is 429 `too_many_inboxes`. Extending an inbox you already hold costs nothing against that cap, which makes `extend` the intended answer to a 429.',
7504
+ description: 'Mints a disposable address and resolves with the inbox plus its `token`. It needs no credential, and only this call and `extend` return a token. The token is the lease itself, signed, and nothing about it is stored on the server, so a lost token cannot be recovered, and every other method except `listDomains` needs it. Store it before you show the address to anyone.\n\nThe body is optional and so is every field in it. With nothing, you get a 12 character generated local part on the first domain in the pool, leased for 60 minutes. A chosen `localPart` is lowercased and must be up to 64 letters, digits, dots, dashes or underscores, starting and ending with a letter or digit, or it is 422 `invalid_address`, and one longer than 64 characters is 422 `invalid_parameter`. Names such as `postmaster`, `abuse` and `support` are 422 `reserved_address`, and an install with no pooled domain answers 503 `not_configured`.\n\nNothing is rate limited and nothing reserves an address, so this never answers 429 or 409. A name you choose is issued to anyone who asks for it, and each of you reads the mail that reaches it from the start of your own lease. Leave `localPart` out when the mail should reach you alone.',
7504
7505
  method: 'POST',
7505
7506
  path: '/temp-mail/inboxes',
7506
7507
  scopes: [],
7507
7508
  auth: 'none',
7508
7509
  parameters: [
7509
- { name: 'body.domain', type: 'string', required: false, description: 'A domain from `listDomains`. Omit it for a random pick. Any other name is 422 `unknown_domain` rather than a silent substitute.' },
7510
- { name: 'body.localPart', type: 'string', required: false, description: 'The part before the @. Omit it for a generated one, which cannot collide.' },
7510
+ { name: 'body.domain', type: 'string', required: false, description: 'A domain from `listDomains`. Omit it for the first one in the pool. Any other name is 422 `unknown_domain` rather than a silent substitute.' },
7511
+ { name: 'body.localPart', type: 'string', required: false, description: 'The part before the @. Omit it for a generated one, which nobody else is likely to be issued.' },
7511
7512
  { name: 'body.ttlMinutes', type: 'number', required: false, description: 'Lease length from now, a whole number from 1 to 1440. Defaults to 60. Out of range is 422 `invalid_parameter`, not clamped.' },
7512
7513
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7513
7514
  { name: 'options.inboxToken', type: 'string', required: false, description: 'Ignored and never sent. Creating an inbox is anonymous and hands you the token instead.' }
@@ -7516,8 +7517,8 @@ const TempMailCreateMethod = {
7516
7517
  example: 'const temp = createTempMail()\n\nconst inbox = await temp.create({ ttlMinutes: 120 })\n\nconsole.log(inbox.address, inbox.expiresAt)\nconsole.log(inbox.token)',
7517
7518
  notes: [
7518
7519
  'Not retried automatically. A retry would mint a second inbox, and the first would be unreachable because its only token was in the lost response.',
7519
- 'A destroyed or expired address stays reserved until 7 days after its lease end, so asking for it again in that window is 409 `address_taken`.',
7520
- 'A lease of 1440 minutes reaches the 24 hour ceiling at once, so the inbox comes back with `extensionsLeft: 0`.',
7520
+ 'Nothing holds an address back after its lease ends or its inbox is deleted, so it can be issued again at once, to anybody.',
7521
+ 'A lease of 1440 minutes reaches the 24 hour ceiling at once, yet the inbox still comes back with `extensionsLeft: 23`, because that counts calls rather than time. None of them can add a minute.',
7521
7522
  'Unknown body keys are 422 `invalid_parameter`, while a body that is not valid JSON is treated as an empty one.'
7522
7523
  ]
7523
7524
  };
@@ -7529,21 +7530,21 @@ const TempMailDeleteMessageMethod = {
7529
7530
  slug: 'temp-mail/deleteMessage',
7530
7531
  signature: 'deleteMessage(inboxId: string, messageId: string, options?: InboxScope): Promise<DeletedTempMessageResource>',
7531
7532
  summary: 'Delete one message from a disposable inbox',
7532
- description: 'Removes the message row, its stored body and its attachments immediately. There is no undo and no bin, since a disposable inbox has no folders.\n\n`messageCount` goes down by one. There is never a slot to free: an inbox keeps every message that reaches it, and `listMessages` pages through all of them.',
7533
+ description: 'Moves the message, attachments and all, to the bin of the mailbox that runs the pool, and no lease lists or opens it again. Nothing in this SDK restores it.\n\n`messageCount` goes down by one. There is never a slot to free: an inbox keeps every message that reaches it, and `listMessages` pages through all of them.',
7533
7534
  method: 'DELETE',
7534
7535
  path: '/temp-mail/inboxes/{id}/messages/{messageId}',
7535
7536
  scopes: [],
7536
7537
  auth: 'inboxToken',
7537
7538
  parameters: [
7538
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7539
- { name: 'messageId', type: 'string', required: true, description: 'A `tmail_` id from `listMessages`.' },
7539
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7540
+ { name: 'messageId', type: 'string', required: true, description: 'An `id` from `listMessages`, such as `thr_` and 24 hex.' },
7540
7541
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7541
7542
  { name: 'options.inboxToken', type: 'string', required: false, description: 'The `oe_inbox_` token `create` returned. Overrides the token given to `createTempMail` for this call, and is required on the `OpenEmail` client, which would otherwise send its API key.' }
7542
7543
  ],
7543
7544
  returns: '`DeletedTempMessageResource` with `object` set to `temp_message`, the message `id` and `deleted: true`.',
7544
7545
  example: 'const temp = createTempMail()\n\nconst inbox = await temp.create()\n\nconst { items } = await temp.listMessages(inbox.id, { inboxToken: inbox.token })\n\nfor (const message of items.filter((row) => row.spam)) {\n await temp.deleteMessage(inbox.id, message.id, { inboxToken: inbox.token })\n}',
7545
7546
  notes: [
7546
- 'An unknown or already deleted message id is 404 `resource_not_found`.',
7547
+ 'An unknown or already deleted message id is 404 `resource_not_found`, and an install with no key to read the pool answers 503 `not_configured`.',
7547
7548
  'Not retried automatically. If you repeat it yourself after a lost response, that 404 means the first attempt already worked.'
7548
7549
  ]
7549
7550
  };
@@ -7554,14 +7555,14 @@ const TempMailDeleteMethod = {
7554
7555
  accessor: 'openemail.tempMail.delete',
7555
7556
  slug: 'temp-mail/delete',
7556
7557
  signature: 'delete(inboxId: string, options?: InboxScope): Promise<DeletedTempInboxResource>',
7557
- summary: 'Destroy a disposable inbox and its mail now',
7558
- description: 'Deletes every message, stored body and attachment in the inbox immediately rather than at the next sweep, and the token stops authenticating on the very next request. There is no undo and nothing is archived.\n\nThe address is not released. The inbox is kept as a tombstone until 7 days after its lease would have ended, so the local part cannot be issued to someone else while a slow sender may still be delivering to it. Creating the same address in that window is 409 `address_taken`, and mail that arrives for it is dropped.\n\nThe response is a tombstone rather than an empty body, so a log line can name what went.',
7558
+ summary: 'Move the mail in a disposable inbox to the bin now',
7559
+ description: 'Moves every message the inbox shows to the bin of the mailbox that runs the pool, at once. It does not end the lease: nothing about a lease is stored, so there is nothing to revoke, and the token keeps opening the address until its expiry. Mail that arrives afterwards is listed as usual.\n\nNothing holds the address back either, so it can be issued again at once, to anybody. An install with no key to read the pool answers 503 `not_configured`.\n\nThe response is a tombstone rather than an empty body, so a log line can name what went.',
7559
7560
  method: 'DELETE',
7560
7561
  path: '/temp-mail/inboxes/{id}',
7561
7562
  scopes: [],
7562
7563
  auth: 'inboxToken',
7563
7564
  parameters: [
7564
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7565
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7565
7566
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7566
7567
  { name: 'options.inboxToken', type: 'string', required: false, description: 'The `oe_inbox_` token `create` returned. Overrides the token given to `createTempMail` for this call, and is required on the `OpenEmail` client, which would otherwise send its API key.' }
7567
7568
  ],
@@ -7569,7 +7570,7 @@ const TempMailDeleteMethod = {
7569
7570
  example: 'const temp = createTempMail()\n\nconst inbox = await temp.create()\n\nconst result = await temp.delete(inbox.id, { inboxToken: inbox.token })\n\nconsole.log(result.destroyed)',
7570
7571
  notes: [
7571
7572
  'The flag is `destroyed`, not `deleted` as on other tombstones.',
7572
- 'Not retried automatically. If you repeat it yourself after a lost response, a 404 `resource_not_found` means the first attempt already destroyed the inbox.'
7573
+ 'Not retried automatically. Repeating it is harmless: the lease still stands, so a second call answers 200 and moves whatever has arrived since.'
7573
7574
  ]
7574
7575
  };
7575
7576
 
@@ -7578,23 +7579,23 @@ const TempMailExtendMethod = {
7578
7579
  name: 'extend',
7579
7580
  accessor: 'openemail.tempMail.extend',
7580
7581
  slug: 'temp-mail/extend',
7581
- signature: 'extend(inboxId: string, options?: InboxScope): Promise<TempInboxResource>',
7582
+ signature: 'extend(inboxId: string, options?: InboxScope): Promise<ExtendedTempInboxResource>',
7582
7583
  summary: 'Push the expiry of an inbox an hour further out',
7583
- description: 'Adds up to 60 minutes to `expiresAt` and resolves with the updated inbox. There is no body.\n\nThe cap is on the whole lease, not on the number of calls. The new expiry is the earlier of one hour past the current expiry and 24 hours after `createdAt`, and a lease allows at most 23 extensions. The last extension therefore often buys less than an hour, and an inbox created with a long `ttlMinutes` has fewer to spend. `extensionsLeft` accounts for both limits, so read it from the response rather than counting calls.\n\nWhen `extensionsLeft` is 0 this answers 409 `extension_limit` for good, and the only way on is a new inbox. Extending does not reset the 50 message ceiling and does not count against the minting limit on `create`.',
7584
+ description: 'Adds up to 60 minutes to `expiresAt` and resolves with the updated inbox and a new `token` that carries the later expiry. There is no body. The old token keeps its old expiry, so use the new one from here on, including in a client built with `createTempMail({ inboxToken })`.\n\nThe new expiry is the earlier of one hour past the current expiry and 24 hours after `createdAt`, and a lease allows at most 23 extensions. The last extension can buy less than an hour, and on a lease that already reaches the 24 hours a call still succeeds, spends an extension and buys nothing. `extensionsLeft` counts only the 23 calls, so compare `expiresAt` with `createdAt` before offering more time.\n\nWhen `extensionsLeft` is 0 this answers 422 `extension_limit` for good, and the only way on is a new inbox. The response reads no mail, so its `messageCount` is 0 and its `lastMessageAt` is null.',
7584
7585
  method: 'POST',
7585
7586
  path: '/temp-mail/inboxes/{id}/extend',
7586
7587
  scopes: [],
7587
7588
  auth: 'inboxToken',
7588
7589
  parameters: [
7589
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7590
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7590
7591
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7591
7592
  { name: 'options.inboxToken', type: 'string', required: false, description: 'The `oe_inbox_` token `create` returned. Overrides the token given to `createTempMail` for this call, and is required on the `OpenEmail` client, which would otherwise send its API key.' }
7592
7593
  ],
7593
- returns: '`TempInboxResource` with the new `expiresAt` and the updated `extensionsLeft`.',
7594
- example: 'const temp = createTempMail({ inboxToken: \'oe_inbox_Vb3kT9qLm2Xw7RzN4pYc6HfJ1sGa5Ed8KuQo0iWnS2e\' })\n\nconst inbox = await temp.extend(\'tinb_5d2a8f1c7e4b9a30d6c1f8e2\')\n\nconsole.log(inbox.expiresAt, inbox.extensionsLeft)',
7594
+ returns: '`ExtendedTempInboxResource`: the `TempInboxResource` fields with the new `expiresAt` and the updated `extensionsLeft`, plus the new `token` beginning `oe_inbox_`.',
7595
+ example: 'const temp = createTempMail({ inboxToken: \'oe_inbox_Vb3kT9qLm2Xw7RzN4pYc6HfJ1sGa5Ed8KuQo0iWnS2e\' })\n\nconst inbox = await temp.extend(\'tinb_k7m2q9xw4bdp\')\n\nconsole.log(inbox.expiresAt, inbox.extensionsLeft)\n\nconst renewed = createTempMail({ inboxToken: inbox.token })',
7595
7596
  notes: [
7596
7597
  'Not retried automatically, because a replay would spend a second extension.',
7597
- 'An inbox created with `ttlMinutes: 1440` has `extensionsLeft: 0` from the start.'
7598
+ 'An inbox created with `ttlMinutes: 1440` still reports `extensionsLeft: 23`, and none of them can add a minute.'
7598
7599
  ]
7599
7600
  };
7600
7601
 
@@ -7605,21 +7606,21 @@ const TempMailGetMessageMethod = {
7605
7606
  slug: 'temp-mail/getMessage',
7606
7607
  signature: 'getMessage(inboxId: string, messageId: string, options?: InboxScope): Promise<TempMessageDetailResource>',
7607
7608
  summary: 'Read one message with its stored body',
7608
- description: 'Resolves the list row for one message plus the parsed message as stored. Reading marks the message seen, so `seen` on this response is always true and the next `listMessages` agrees. There is no way to read without marking, and nothing on the server depends on the flag.\n\nRender `message.decodedBody`. `body` and `processedHtml` are empty strings for every message that can reach a disposable inbox, so a client reading either shows a blank page. Bodies over 2 MB are cut at that size, so check `truncated` before treating the content as the whole message.\n\nThe HTML came from a stranger to an address anyone could name. Render it outside your own origin, for example in a sandboxed iframe.',
7609
+ description: 'Resolves the list row for one message plus the parsed message as stored. Reading it does not mark it seen: `seen` mirrors the unread state of the message in the mailbox that runs the pool, and nothing on these routes changes it.\n\nRender `message.decodedBody`. `body` and `processedHtml` are empty strings for every message that can reach a disposable inbox, so a client reading either shows a blank page. The body is never cut, so `truncated` is always false.\n\nThe HTML came from a stranger to an address anyone could name. Render it outside your own origin, for example in a sandboxed iframe.',
7609
7610
  method: 'GET',
7610
7611
  path: '/temp-mail/inboxes/{id}/messages/{messageId}',
7611
7612
  scopes: [],
7612
7613
  auth: 'inboxToken',
7613
7614
  parameters: [
7614
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7615
- { name: 'messageId', type: 'string', required: true, description: 'A `tmail_` id from `listMessages`.' },
7615
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7616
+ { name: 'messageId', type: 'string', required: true, description: 'An `id` from `listMessages`, such as `thr_` and 24 hex.' },
7616
7617
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7617
7618
  { name: 'options.inboxToken', type: 'string', required: false, description: 'The `oe_inbox_` token `create` returned. Overrides the token given to `createTempMail` for this call, and is required on the `OpenEmail` client, which would otherwise send its API key.' }
7618
7619
  ],
7619
7620
  returns: '`TempMessageDetailResource`: every `TempMessageResource` field plus `message`, the stored parsed message, and `truncated`.',
7620
- example: 'const temp = createTempMail({ inboxToken: \'oe_inbox_Vb3kT9qLm2Xw7RzN4pYc6HfJ1sGa5Ed8KuQo0iWnS2e\' })\n\nconst detail = await temp.getMessage(\'tinb_5d2a8f1c7e4b9a30d6c1f8e2\', \'tmail_9e3b7c1a5f2d8e40b6a9c3f1\')\n\nconsole.log(detail.subject, detail.truncated)\nconsole.log(detail.message.decodedBody)',
7621
+ example: 'const temp = createTempMail({ inboxToken: \'oe_inbox_Vb3kT9qLm2Xw7RzN4pYc6HfJ1sGa5Ed8KuQo0iWnS2e\' })\n\nconst detail = await temp.getMessage(\'tinb_k7m2q9xw4bdp\', \'thr_9e3b7c1a5f2d8e40b6a9c3f1\')\n\nconsole.log(detail.subject, detail.truncated)\nconsole.log(detail.message.decodedBody)',
7621
7622
  notes: [
7622
- 'An unknown or deleted message id is 404 `resource_not_found`, the same answer as an expired inbox.',
7623
+ 'A message this lease cannot see, deleted ones included, is 404 `resource_not_found`. An expired lease is 401 `inbox_expired`, and an install with no key to read the pool answers 503 `not_configured`.',
7623
7624
  '`message` is typed as a loose record, so narrow `decodedBody` to a string before rendering it.'
7624
7625
  ]
7625
7626
  };
@@ -7631,20 +7632,20 @@ const TempMailGetMethod = {
7631
7632
  slug: 'temp-mail/get',
7632
7633
  signature: 'get(inboxId: string, options?: InboxScope): Promise<TempInboxResource>',
7633
7634
  summary: 'Read the lease and counters of a disposable inbox',
7634
- description: 'Resolves the inbox with its expiry, remaining extensions and message counters, without any messages. To watch an inbox, poll `listMessages` instead: it returns `expiresAt` alongside the mail, so one request covers both.\n\nThis call is authorised by the inbox token, never by an API key. A workspace key sent in its place is refused with 401 `invalid_credential_type`, and no scope on any key reaches this resource. The id in the path must be the inbox the token belongs to.\n\nOnce the lease is over the inbox is gone rather than read only. An expired, destroyed, foreign or unknown inbox all answer the same 404 `resource_not_found`.',
7635
+ description: 'Resolves the inbox with its expiry, remaining extensions and message counters, without any messages. To watch an inbox, poll `listMessages` instead: it returns `expiresAt` alongside the mail, so one request covers both.\n\nThis call is authorised by the inbox token, never by an API key. A workspace key sent in its place is refused with 401 `missing_inbox_token`, and no scope on any key reaches this resource. The token alone decides which inbox is read, and the id in the path is not checked against it.\n\nOnce the lease is over the token answers 401 `inbox_expired`, and a token this server did not sign answers 404 `resource_not_found`. Deleting an inbox does not end its lease, so its token still reads it. `messageCount` is counted by reading every page, and is 0 when the install cannot read the mailbox that runs the pool.',
7635
7636
  method: 'GET',
7636
7637
  path: '/temp-mail/inboxes/{id}',
7637
7638
  scopes: [],
7638
7639
  auth: 'inboxToken',
7639
7640
  parameters: [
7640
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7641
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7641
7642
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7642
7643
  { name: 'options.inboxToken', type: 'string', required: false, description: 'The `oe_inbox_` token `create` returned. Overrides the token given to `createTempMail` for this call, and is required on the `OpenEmail` client, which would otherwise send its API key.' }
7643
7644
  ],
7644
7645
  returns: '`TempInboxResource` with `id`, `address`, `domain`, `createdAt`, `expiresAt`, `extensionsLeft`, `messageCount`, `messageLimit` and `lastMessageAt`.',
7645
7646
  example: 'const temp = createTempMail()\n\nconst created = await temp.create()\n\nconst inbox = await temp.get(created.id, { inboxToken: created.token })\n\nconsole.log(inbox.expiresAt, inbox.messageCount, inbox.messageLimit)',
7646
7647
  notes: [
7647
- 'A token that is mistyped or not an inbox token at all is a 404, not a 401. Only a missing token and a workspace API key get 401.',
7648
+ 'A token that does not start with `oe_inbox_` is 401 `missing_inbox_token`, the same as no token at all. One that starts with it but was not signed by this server is 404 `resource_not_found`.',
7648
7649
  '`messageCount` counts every message the inbox is showing, across every page, and goes down when one is deleted. `messageLimit`, which is 50, is the page size of `listMessages`, not a ceiling: nothing past it is dropped.'
7649
7650
  ]
7650
7651
  };
@@ -7708,23 +7709,23 @@ const TempMailListAttachmentsMethod = {
7708
7709
  name: 'listAttachments',
7709
7710
  accessor: 'openemail.tempMail.listAttachments',
7710
7711
  slug: 'temp-mail/listAttachments',
7711
- signature: 'listAttachments(inboxId: string, messageId: string, options?: InboxScope): Promise<Array<AttachmentResource>>',
7712
- summary: 'Read the attachments of a message with their bytes',
7713
- description: 'Resolves every attachment on a message as a plain array, metadata and content together, with the bytes base64 encoded in `content`. There is no per attachment fetch.\n\n`content` is null when a part was over the 8 MB storage ceiling and its bytes were never kept. That is not an empty file, and the metadata is still returned so a client can say the attachment was too large to keep. `filename` and `contentType` are whatever the sender declared.\n\nNothing here is scanned. The bytes came from a stranger to an address anyone could name, so open or render them outside your own origin.',
7712
+ signature: 'listAttachments(inboxId: string, messageId: string, options?: InboxScope): Promise<Array<TempAttachmentResource>>',
7713
+ summary: 'List the attachments of a message, metadata only',
7714
+ description: 'Resolves every attachment on a message as a plain array of metadata: `attachmentId`, `filename`, `mimeType` and `size` in decoded bytes, with `body` an empty string and `headers` empty. No route on a disposable inbox serves the bytes, and there is no per attachment fetch.\n\n`filename` and `mimeType` are whatever the sender declared, and nothing here is scanned. A message this lease cannot see is 404 `resource_not_found`, and an install with no key to read the pool answers 503 `not_configured`.',
7714
7715
  method: 'GET',
7715
7716
  path: '/temp-mail/inboxes/{id}/messages/{messageId}/attachments',
7716
7717
  scopes: [],
7717
7718
  auth: 'inboxToken',
7718
7719
  parameters: [
7719
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7720
- { name: 'messageId', type: 'string', required: true, description: 'A `tmail_` id from `listMessages`.' },
7720
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7721
+ { name: 'messageId', type: 'string', required: true, description: 'An `id` from `listMessages`, such as `thr_` and 24 hex.' },
7721
7722
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7722
7723
  { name: 'options.inboxToken', type: 'string', required: false, description: 'The `oe_inbox_` token `create` returned. Overrides the token given to `createTempMail` for this call, and is required on the `OpenEmail` client, which would otherwise send its API key.' }
7723
7724
  ],
7724
- returns: '`Array<AttachmentResource>`, each with `attachmentId`, `filename`, `contentType`, `size` in decoded bytes and `content` as base64 or null.',
7725
- example: 'const temp = createTempMail({ inboxToken: \'oe_inbox_Vb3kT9qLm2Xw7RzN4pYc6HfJ1sGa5Ed8KuQo0iWnS2e\' })\n\nconst attachments = await temp.listAttachments(\'tinb_5d2a8f1c7e4b9a30d6c1f8e2\', \'tmail_9e3b7c1a5f2d8e40b6a9c3f1\')\n\nconst pdf = attachments.find((file) => file.contentType === \'application/pdf\' && file.content !== null)\n\nconsole.log(pdf?.filename, pdf?.size)',
7725
+ returns: '`Array<TempAttachmentResource>`, each with `attachmentId`, `filename`, `mimeType`, `size` in decoded bytes, `body` as an empty string and `headers` as an empty array.',
7726
+ example: 'const temp = createTempMail({ inboxToken: \'oe_inbox_Vb3kT9qLm2Xw7RzN4pYc6HfJ1sGa5Ed8KuQo0iWnS2e\' })\n\nconst attachments = await temp.listAttachments(\'tinb_k7m2q9xw4bdp\', \'thr_9e3b7c1a5f2d8e40b6a9c3f1\')\n\nfor (const file of attachments) console.log(file.filename, file.size)',
7726
7727
  notes: [
7727
- 'Reading attachments marks the message seen, the same as `getMessage`.',
7728
+ 'Listing attachments does not mark the message seen.',
7728
7729
  'Check `attachmentCount` from `listMessages` first to skip this call for messages with none.'
7729
7730
  ]
7730
7731
  };
@@ -7736,7 +7737,7 @@ const TempMailListDomainsMethod = {
7736
7737
  slug: 'temp-mail/listDomains',
7737
7738
  signature: 'listDomains(options?: InboxScope): Promise<Array<TempDomainResource>>',
7738
7739
  summary: 'List the domains a disposable inbox can be created on',
7739
- description: 'Resolves the pool of domains `create` accepts, as a plain array in the order an operator set for the picker. It takes no credential at all: no API key and no inbox token are sent, even when the client holding this namespace has one.\n\nOnly domains that are enrolled, enabled and verified are listed, because an unverified domain receives nothing and an address on it would never work. An empty array is a normal answer meaning this install offers no disposable domains, and it is exactly the condition under which `create` fails with 503 `temp_mail_unavailable`.',
7740
+ description: 'Resolves the pool of domains `create` accepts, as a plain array in the order the operator wrote them in `TEMP_MAIL_DOMAINS`. It takes no credential at all: no API key and no inbox token are sent, even when the client holding this namespace has one.\n\nNothing checks that a listed domain is verified, so the list is only as good as the operator made it. An empty array is a normal answer meaning this install offers no disposable domains, and it is exactly the condition under which `create` fails with 503 `not_configured`.',
7740
7741
  method: 'GET',
7741
7742
  path: '/temp-mail/domains',
7742
7743
  scopes: [],
@@ -7748,7 +7749,7 @@ const TempMailListDomainsMethod = {
7748
7749
  returns: '`Array<TempDomainResource>`, each with `object` set to `temp_domain` and a lowercased `domain` to pass back to `create`.',
7749
7750
  example: 'const temp = createTempMail()\n\nconst domains = await temp.listDomains()\n\nconsole.log(domains.map((entry) => entry.domain))',
7750
7751
  notes: [
7751
- 'The server caches the pool for up to 60 seconds, so a domain an operator just enrolled can take a minute to appear.',
7752
+ 'The pool is server configuration, so a domain appears when the operator adds it to `TEMP_MAIL_DOMAINS`, not when it is added to a workspace.',
7752
7753
  'Retried automatically on network failure and retryable statuses, like every GET.'
7753
7754
  ]
7754
7755
  };
@@ -7760,13 +7761,13 @@ const TempMailListMessagesMethod = {
7760
7761
  slug: 'temp-mail/listMessages',
7761
7762
  signature: 'listMessages(inboxId: string, options?: TempMessageListOptions): Promise<TempMessagesResource>',
7762
7763
  summary: 'List one page of the messages in a disposable inbox',
7763
- description: 'Resolves one page of the messages in the inbox newest first, by the time the server received them, together with the inbox `expiresAt`. Rows carry metadata only, and the call reads no object storage, so polling it every few seconds is the cheap way to wait for a confirmation email.\n\nA page holds up to 50 messages. When more have arrived, `hasMore` is true and `nextCursor` goes back as `options.cursor` for the next page, so nothing that reached the inbox is hidden; `listAllMessages` and `iterateMessages` do that walk for you. `snippet` is plain text capped at 400 characters, which is often enough to read a one time code without opening the message.\n\n`spam` is a flag, never a filing decision. A machine sent confirmation from a sender with no reputation is exactly what a disposable inbox exists to receive, so flagged messages are still listed. `from` is whatever the message claimed and has not been authenticated.',
7764
+ description: 'Resolves one page of the messages in the inbox newest first, by the time the server received them, together with the inbox `expiresAt`. Rows carry metadata only, and polling this is the way to wait for a confirmation email. Only mail delivered to this address since the lease began is listed.\n\nA page holds up to 50 messages. When more have arrived, `hasMore` is true and `nextCursor` goes back as `options.cursor` for the next page, so nothing that reached the inbox is hidden; `listAllMessages` and `iterateMessages` do that walk for you. A page can hold fewer than `limit` rows, even none, while `hasMore` is true, because mail to other addresses on the pool is read and dropped. `snippet` is plain text capped at 400 characters, which is often enough to read a one time code without opening the message.\n\n`spam` is a flag, never a filing decision. A machine sent confirmation from a sender with no reputation is exactly what a disposable inbox exists to receive, so flagged messages are still listed. `from` is whatever the message claimed and has not been authenticated.',
7764
7765
  method: 'GET',
7765
7766
  path: '/temp-mail/inboxes/{id}/messages',
7766
7767
  scopes: [],
7767
7768
  auth: 'inboxToken',
7768
7769
  parameters: [
7769
- { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It must name the same inbox as the token, or the answer is a 404.' },
7770
+ { name: 'inboxId', type: 'string', required: true, description: 'The `tinb_` id `create` returned. It is not checked against the token, which alone decides the inbox.' },
7770
7771
  { name: 'options.limit', type: 'number', required: false, description: 'Messages per page, a whole number from 1 to 50, defaulting to 50. Out of range is 422 `invalid_parameter`.' },
7771
7772
  { name: 'options.cursor', type: 'string', required: false, description: 'The `nextCursor` from the previous page. Never build one yourself.' },
7772
7773
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
@@ -7778,7 +7779,8 @@ const TempMailListMessagesMethod = {
7778
7779
  notes: [
7779
7780
  'A message whose `Message-ID` header matches one already in the inbox is not stored twice.',
7780
7781
  '`to` is the inbox address. A `+tag` the sender added is folded back into the base address.',
7781
- 'Every message the inbox has received is listed, a page at a time. `messageLimit` on the inbox is the size the tool is built for, not a point past which mail is hidden.'
7782
+ 'Every message the inbox has received is listed, a page at a time. `messageLimit` on the inbox is the size the tool is built for, not a point past which mail is hidden.',
7783
+ 'An install with no key to read the pool answers 503 `not_configured`.'
7782
7784
  ]
7783
7785
  };
7784
7786
 
@@ -13661,6 +13663,7 @@ const RESOURCE_ONE_TIME_SECRETS = {
13661
13663
  const RESOURCE_TEMP_INBOX = {
13662
13664
  NAMESPACE: 'tempMail',
13663
13665
  CREATE: 'create',
13666
+ EXTEND: 'extend',
13664
13667
  DELETE: 'delete',
13665
13668
  TOKEN_FIELD: 'token'
13666
13669
  };
@@ -13818,6 +13821,12 @@ const RESOURCE_TYPE_COLUMNS = {
13818
13821
  TempMessagesResource: TEMP_MESSAGE_COLUMNS,
13819
13822
  TempMessageResource: TEMP_MESSAGE_COLUMNS,
13820
13823
  TempDomainResource: [Column('domain', 'domain')],
13824
+ TempAttachmentResource: [
13825
+ Column('id', 'attachmentId', RENDER_CELL_FORMATS.ID),
13826
+ Column('filename', 'filename'),
13827
+ Column('type', 'mimeType'),
13828
+ Column('size', 'size', RENDER_CELL_FORMATS.BYTES)
13829
+ ],
13821
13830
  ApiKeyRequestResource: [
13822
13831
  Column('id', 'id', RENDER_CELL_FORMATS.ID),
13823
13832
  Column('method', 'method'),
@@ -13921,6 +13930,7 @@ const RESOURCE_ID_LIST_KEY = 'ids';
13921
13930
  const RESOURCE_ID_LIST_SUFFIX = 'Ids';
13922
13931
  const RESOURCE_MAX_SAMPLE = '100';
13923
13932
  const RESOURCE_READ_METHOD = 'GET';
13933
+ const RESOURCE_PATCH_METHOD = 'PATCH';
13924
13934
  const RESOURCE_EXAMPLE_LIMIT = 5;
13925
13935
  const RESOURCE_EXAMPLE_OPTIONAL_FLAGS = 3;
13926
13936
  const RESOURCE_CONFIRM_TARGET_LIMIT = 5;
@@ -13941,6 +13951,7 @@ const RESOURCE_UNSUPPORTED_ESCAPE = /\\[^'\\]/;
13941
13951
  const RESOURCE_SECRET_FLAG = /(key|token|secret|password)$/;
13942
13952
  const RESOURCE_SENTENCE_BREAK = /(?<=[.!?])\s+(?=[A-Z`])/;
13943
13953
  const RESOURCE_CAMEL_PATH = /^[a-z]+[A-Z][A-Za-z]*$/;
13954
+ const RESOURCE_EMPTY_STRING_CLEAR = /(\b[Nn]ull\b|`null`) or an empty string/g;
13944
13955
  const RESOURCE_EXAMPLE_PATTERNS = {
13945
13956
  STRING: /(\w+):\s*'((?:[^'\\\n]|\\.)*)'/g,
13946
13957
  NUMBER: /(\w+):\s*(-?\d+(?:\.\d+)?)(?=\s*[,}\n])/g,
@@ -13975,10 +13986,11 @@ const RESOURCE_HELP = {
13975
13986
  NOTES: 'NOTES',
13976
13987
  NO_SCOPES: 'Any key or sign-in for the workspace.',
13977
13988
  SIGNED_OUT: 'None. It works signed out, with no key.',
13978
- INBOX_TOKEN: 'None. The inbox token authorises it: `--inbox-token`, or the token the CLI saved when it created the inbox.',
13989
+ INBOX_TOKEN: 'None. The inbox token authorises it: `--inbox-token`, or the token the CLI saved when it created or last extended the inbox.',
13979
13990
  REQUIRED: 'Required.',
13980
13991
  REQUIRED_WITH_DATA: 'Required, here or in `--data`.',
13981
13992
  PAGING: 'Add `--all` to walk every page: a table on a terminal, one JSON object per line when piped or with `--ndjson`, and one `{ items, hasMore, nextCursor }` document with `--json`. `--max <n>` stops after that many items.',
13993
+ PAGING_OWN_SHAPE: 'Add `--all` to walk every page: a table on a terminal, one JSON object per line when piped or with `--ndjson`, and one JSON document with `--json`, shaped as Returns describes. `--max <n>` stops after that many items.',
13982
13994
  DATA_USAGE: '--data <json|@file|->',
13983
13995
  JSON_TYPE: (type) => `JSON shaped as \`${type}\`, inline or from a file with @path.`
13984
13996
  };
@@ -14004,6 +14016,7 @@ const RESOURCE_MESSAGES = {
14004
14016
  INBOX_TOKEN_REJECTED: 'The inbox token was refused.',
14005
14017
  INBOX_TOKEN_REJECTED_NEXT: 'Check `--inbox-token`, or create a new inbox with `openemail temp new`.',
14006
14018
  INBOX_SAVED: (address) => `Saved the inbox token for ${address}, so later temp-mail commands find it.`,
14019
+ INBOX_RENEWED: (address) => `Saved the new inbox token for ${address}, which carries the later expiry, so later temp-mail commands use it.`,
14007
14020
  SECRET_IN_ARGV: (label) => `${label} was passed on the command line, where your shell history and the process list can show it. Next time pass \`-\` to read it from standard input, or \`@path\` to read it from a file.`,
14008
14021
  NO_KEYLESS_RESOURCE: (namespace) => `The ${namespace} commands need a credential.`,
14009
14022
  NO_METHOD: (accessor) => `${accessor} is not in the SDK this CLI was built with.`,
@@ -14104,13 +14117,13 @@ const TEMP_FLAGS = {
14104
14117
  name: TEMP_FLAG_NAMES.DOMAIN,
14105
14118
  kind: FLAG_KINDS.STRING,
14106
14119
  placeholder: 'domain',
14107
- description: 'Domain from `openemail temp-mail list-domains`. Left out, one is picked at random'
14120
+ description: 'Domain from `openemail temp-mail list-domains`. Left out, the first one in that list is used'
14108
14121
  },
14109
14122
  NAME: {
14110
14123
  name: TEMP_FLAG_NAMES.NAME,
14111
14124
  kind: FLAG_KINDS.STRING,
14112
14125
  placeholder: 'local-part',
14113
- description: 'The part before the @, 3 to 32 letters, digits, dots, dashes or underscores. Left out, one is generated'
14126
+ description: 'The part before the @, up to 64 letters, digits, dots, dashes or underscores, starting and ending with a letter or digit. Left out, one is generated. Anyone who asks for the same name reads its mail too'
14114
14127
  },
14115
14128
  TTL: {
14116
14129
  name: TEMP_FLAG_NAMES.TTL,
@@ -14142,7 +14155,10 @@ const TEMP_LIMITS = {
14142
14155
  SNIPPET_WIDTH: 60
14143
14156
  };
14144
14157
  const TEMP_TOKEN_PREFIX = 'oe_inbox_';
14145
- const TEMP_MESSAGE_PREFIX = 'tmail_';
14158
+ const TEMP_MESSAGE_PREFIX = 'thr_';
14159
+ const TEMP_API_ERROR_CODES = {
14160
+ INBOX_EXPIRED: 'inbox_expired'
14161
+ };
14146
14162
  const TEMP_COLUMNS = {
14147
14163
  ID: 'Inbox',
14148
14164
  ADDRESS: 'Address',
@@ -14157,7 +14173,7 @@ const TEMP_MESSAGES = {
14157
14173
  CREATING: 'Creating a temporary inbox',
14158
14174
  LOADING: 'Loading messages',
14159
14175
  LOADING_MESSAGE: 'Loading the message',
14160
- DELETING: 'Deleting the inbox',
14176
+ DELETING: 'Moving the mail to the bin',
14161
14177
  NO_INBOXES: 'You have no temporary inboxes.',
14162
14178
  NO_INBOXES_NEXT: 'Run `openemail temp new` to create one.',
14163
14179
  PICK_INBOX: 'Which inbox?',
@@ -14166,12 +14182,9 @@ const TEMP_MESSAGES = {
14166
14182
  TTL_RANGE: '--ttl takes a whole number of minutes from 1 to 1440.',
14167
14183
  INVALID_TOKEN: '--inbox-token must be an inbox token beginning oe_inbox_.',
14168
14184
  NO_MESSAGES: 'No mail yet.',
14169
- WATCH_STOPPED: 'Stopped watching.',
14170
14185
  EXPIRED: 'The inbox has expired, so there is nothing more to watch.',
14171
- GONE_LOCALLY: 'The inbox was already gone on the server, so it was only forgotten here.',
14172
- TRUNCATED: 'The body is longer than 2 MB, so only the first 2 MB is shown.',
14173
- EMPTY_BODY: '(This message has no body.)',
14174
- UNVERIFIED_SENDER: 'The sender of mail in a temporary inbox is not verified.',
14186
+ GONE_LOCALLY: 'The server no longer accepts this inbox token, so nothing was moved and the inbox was only forgotten here.',
14187
+ LEASE_RUNS_ON: 'The lease runs on until it expires, so mail can still arrive, and nothing holds the address back from anybody else.',
14175
14188
  MORE: (id) => `Older messages are not shown here. Run \`openemail temp-mail list-messages ${id} --all\` to see every one.`,
14176
14189
  ALREADY_HERE: 'Already in the inbox:',
14177
14190
  CREATED: (address) => `Created ${address}.`,
@@ -14181,8 +14194,8 @@ const TEMP_MESSAGES = {
14181
14194
  PRUNED: (count) => `Forgot ${count} expired ${count === 1 ? 'inbox' : 'inboxes'}.`,
14182
14195
  WATCHING: (address) => `Watching ${address}`,
14183
14196
  WAITING: (left) => `Waiting for mail, ${left} left. Press Ctrl+C to stop`,
14184
- CONFIRM_DELETE: (address) => `Delete ${address} and every message in it? This cannot be undone.`,
14185
- DELETED: (address) => `Deleted ${address}.`,
14197
+ CONFIRM_DELETE: (address) => `Move every message in ${address} to the bin and forget the inbox here?`,
14198
+ DELETED: (address) => `Moved the mail in ${address} to the bin and forgot the inbox here.`,
14186
14199
  SENDER_HINT: (id) => `Run \`openemail temp read ${id} <message-id>\` to read one.`
14187
14200
  };
14188
14201
 
@@ -14268,6 +14281,7 @@ const TERMINAL = {
14268
14281
  MIN_WRAP_WIDTH: 16
14269
14282
  };
14270
14283
  const CODE_SPAN_DELIMITER = '`';
14284
+ const CODE_SPAN_PATTERN = /`([^`\s](?:[^`]*[^`\s])?)`/g;
14271
14285
  const BANNER_LINES = [
14272
14286
  ' _ _',
14273
14287
  ' ___ _ __ ___ _ _ ___ _ __ __ _ (_)| |',
@@ -15535,13 +15549,7 @@ const Config$1 = {
15535
15549
  Update: Update$1
15536
15550
  };
15537
15551
 
15538
- const FormatCode = (text, paint) => {
15539
- const parts = text.split(CODE_SPAN_DELIMITER);
15540
- if (parts.length < 3)
15541
- return text;
15542
- const balanced = parts.length % 2 === 1 ? parts : [...parts.slice(0, -2), `${parts[parts.length - 2]}${CODE_SPAN_DELIMITER}${parts[parts.length - 1]}`];
15543
- return balanced.map((part, index) => index % 2 === 1 ? paint(part) : part).join('');
15544
- };
15552
+ const FormatCode = (text, paint) => text.replace(CODE_SPAN_PATTERN, (_span, code) => paint(code));
15545
15553
 
15546
15554
  const KNOWN_KINDS = new Set(Object.values(PROFILE_KINDS));
15547
15555
  const IsUsable = (value) => IsRecord$2(value) && KNOWN_KINDS.has(value.kind) && typeof value.baseUrl === 'string';
@@ -17622,6 +17630,17 @@ const NonEmptyEnv = (name) => {
17622
17630
  return value === undefined || value.length === 0 ? null : value;
17623
17631
  };
17624
17632
 
17633
+ const LOCAL_NETWORK = /^(localhost|127\.\d+\.\d+\.\d+|0\.0\.0\.0|\[::1\]|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|[^/:]+\.local)(:|\/|$)/i;
17634
+
17635
+ const withScheme = (value) => {
17636
+ const trimmed = value.trim();
17637
+ if (trimmed === '' || /^https?:\/\//i.test(trimmed))
17638
+ return trimmed;
17639
+ const bare = trimmed.replace(/^\/+/, '');
17640
+ return `${LOCAL_NETWORK.test(bare) ? 'http' : 'https'}://${bare}`;
17641
+ };
17642
+ const publicUrl = (value) => withScheme(value).replace(/\/+$/, '');
17643
+
17625
17644
  const Invalid = (value) => new UsageError(MESSAGES.INVALID_BASE_URL(Color.Sanitize(value)), { code: CLI_ERROR_CODES.INVALID_VALUE });
17626
17645
  const Parse$3 = (url) => {
17627
17646
  try {
@@ -17641,8 +17660,8 @@ const Unspecified = (normalised, url) => new UsageError(MESSAGES.UNSPECIFIED_HOS
17641
17660
  next: MESSAGES.UNSPECIFIED_HOST_NEXT(Color.Sanitize(LoopbackInstead(url)))
17642
17661
  });
17643
17662
  const NormaliseUrl = (value) => {
17644
- const normalised = NormaliseEndpoint(value);
17645
- if (normalised === undefined)
17663
+ const normalised = publicUrl(value);
17664
+ if (normalised.length === 0)
17646
17665
  throw Invalid(value);
17647
17666
  const url = Parse$3(normalised);
17648
17667
  if (url === null || (url.protocol !== URL_PROTOCOLS.HTTP && url.protocol !== URL_PROTOCOLS.HTTPS))
@@ -18410,6 +18429,17 @@ const Find$1 = (idOrAddress) => {
18410
18429
  return List$2().find(inbox => inbox.id === idOrAddress || inbox.address.toLowerCase() === needle) ?? null;
18411
18430
  };
18412
18431
  const Add = (inbox) => Change(inboxes => [...inboxes.filter(existing => existing.id !== inbox.id), inbox]);
18432
+ const Renew$1 = (id, inboxToken, expiresAt) => {
18433
+ const outcome = { renewed: false };
18434
+ Change(inboxes => {
18435
+ const stored = inboxes.find(inbox => inbox.id === id);
18436
+ if (stored === undefined || Date.parse(expiresAt) < Date.parse(stored.expiresAt))
18437
+ return null;
18438
+ outcome.renewed = true;
18439
+ return inboxes.map(inbox => inbox === stored ? { ...inbox, inboxToken, expiresAt } : inbox);
18440
+ });
18441
+ return outcome.renewed;
18442
+ };
18413
18443
  const Remove$1 = (id) => {
18414
18444
  const outcome = { removed: false };
18415
18445
  Change(inboxes => {
@@ -18433,6 +18463,7 @@ const TempMailStore = {
18433
18463
  List: List$2,
18434
18464
  Find: Find$1,
18435
18465
  Add,
18466
+ Renew: Renew$1,
18436
18467
  Remove: Remove$1,
18437
18468
  PruneExpired
18438
18469
  };
@@ -19257,11 +19288,7 @@ const PathOf = (raw) => {
19257
19288
  return raw;
19258
19289
  }
19259
19290
  };
19260
- const WithoutDocsPrefix = (path) => {
19261
- if (path === DOCS_PATHS.DOCS_PREFIX.slice(0, -1))
19262
- return '';
19263
- return path.startsWith(DOCS_PATHS.DOCS_PREFIX) ? path.slice(DOCS_PATHS.DOCS_PREFIX.length) : path;
19264
- };
19291
+ const WithoutDocsPrefix = (path) => path.replace(DOCS_PATTERNS.DOCS_PREFIX, '');
19265
19292
  const Decoded = (segment) => {
19266
19293
  try {
19267
19294
  return decodeURIComponent(segment);
@@ -25631,6 +25658,8 @@ const Send = {
25631
25658
  }
25632
25659
  };
25633
25660
 
25661
+ const InboxGone = (error) => error instanceof OpenEmailApiError && (error.isNotFound || error.code === TEMP_API_ERROR_CODES.INBOX_EXPIRED);
25662
+
25634
25663
  const FromStored = (inbox) => ({
25635
25664
  id: inbox.id,
25636
25665
  address: inbox.address,
@@ -25697,15 +25726,15 @@ const TempClient = (baseUrl, signal) => createTempMail(SdkBaseOptions(baseUrl, s
25697
25726
  const TempDelete = {
25698
25727
  name: TEMP_COMMAND_NAMES.DELETE,
25699
25728
  aliases: ['rm'],
25700
- summary: 'Delete a disposable inbox and forget it',
25729
+ summary: 'Move the mail in a disposable inbox to the bin and forget its token',
25701
25730
  usage: ['temp delete [inbox] [--yes]'],
25702
- description: 'Deletes every message in the inbox straight away and stops its token working, then removes it from `~/.openemail/temp-mail.json`. The address stays reserved for 7 days after its lease would have ended, so nobody else can take it while late mail may still arrive. You are asked to confirm unless you pass `--yes`.',
25731
+ description: 'Moves every message the inbox shows to the bin at once, then removes the inbox and its token from `~/.openemail/temp-mail.json`. Nothing about the lease is stored on the server, so there is nothing to revoke: it runs on until its expiry, and mail that arrives in the meantime still reaches whoever holds the token. Nothing holds the address back either, so it can be issued again at once, to anybody. You are asked to confirm unless you pass `--yes`.',
25703
25732
  arguments: [{ name: 'inbox', description: 'Inbox id or address from `openemail temp list`, optional when you have only one' }],
25704
25733
  flags: [TEMP_FLAGS.INBOX_TOKEN],
25705
25734
  examples: [
25706
25735
  { command: 'openemail temp delete' },
25707
25736
  { command: 'openemail temp rm quiet-otter-12@example-temp.com --yes' },
25708
- { command: 'openemail temp delete tinb_4f2a --yes --json' }
25737
+ { command: 'openemail temp delete tinb_k7m2q9xw4bdp --yes --json' }
25709
25738
  ],
25710
25739
  auth: COMMAND_AUTHS.NONE,
25711
25740
  destructive: true,
@@ -25716,7 +25745,7 @@ const TempDelete = {
25716
25745
  if (!await ConfirmDestructive(TEMP_MESSAGES.CONFIRM_DELETE(address)))
25717
25746
  return EXIT_CODES.CANCELLED;
25718
25747
  const gone = await CallAnonymous(context, TEMP_MESSAGES.DELETING, signal => TempClient(baseUrl, signal).delete(target.id, { inboxToken: target.token, signal })).then(() => false, (error) => {
25719
- if (error instanceof OpenEmailApiError && error.isNotFound)
25748
+ if (InboxGone(error))
25720
25749
  return true;
25721
25750
  throw error;
25722
25751
  });
@@ -25725,10 +25754,12 @@ const TempDelete = {
25725
25754
  context.io.Json({ object: 'temp_inbox', id: target.id, address: target.address, destroyed: !gone, forgotten });
25726
25755
  return EXIT_CODES.OK;
25727
25756
  }
25728
- if (gone)
25757
+ if (gone) {
25729
25758
  context.io.Warn(TEMP_MESSAGES.GONE_LOCALLY);
25730
- else
25731
- context.io.Success(TEMP_MESSAGES.DELETED(address));
25759
+ return EXIT_CODES.OK;
25760
+ }
25761
+ context.io.Success(TEMP_MESSAGES.DELETED(address));
25762
+ context.io.Hint(TEMP_MESSAGES.LEASE_RUNS_ON);
25732
25763
  return EXIT_CODES.OK;
25733
25764
  }
25734
25765
  };
@@ -25737,7 +25768,7 @@ const TempList = {
25737
25768
  name: TEMP_COMMAND_NAMES.LIST,
25738
25769
  summary: 'List the disposable inboxes this CLI created',
25739
25770
  usage: ['temp list'],
25740
- description: 'Lists the inboxes kept in `~/.openemail/temp-mail.json`, soonest to expire last. Expired inboxes are forgotten as they are found, because the server has already deleted them. Nothing is read from the network.',
25771
+ description: 'Lists the inboxes kept in `~/.openemail/temp-mail.json`, soonest to expire last. Expired inboxes are forgotten as they are found, because their tokens no longer open anything. An inbox extended with `openemail temp-mail extend` shows its later expiry. Nothing is read from the network.',
25741
25772
  examples: [
25742
25773
  { command: 'openemail temp list' },
25743
25774
  { command: 'openemail temp ls --json', note: 'Ids, addresses and expiry times, never the tokens' }
@@ -25784,7 +25815,7 @@ const TempNew = {
25784
25815
  aliases: ['create'],
25785
25816
  summary: 'Create a disposable inbox',
25786
25817
  usage: ['temp new [--name <local-part>] [--domain <domain>] [--ttl <minutes>]'],
25787
- description: 'Creates a disposable address that needs no account and no API key, and keeps its token in `~/.openemail/temp-mail.json` so the other `temp` commands can reach it. The address is printed on stdout on its own, so `ADDRESS=$(openemail temp new)` works in a script. It lives for 60 minutes unless you pass `--ttl`, up to 24 hours.',
25818
+ description: 'Creates a disposable address that needs no account and no API key, and keeps its token in `~/.openemail/temp-mail.json` so the other `temp` commands can reach it. The address is printed on stdout on its own, so `ADDRESS=$(openemail temp new)` works in a script. It lives for 60 minutes unless you pass `--ttl`, up to 24 hours, and `openemail temp-mail extend` adds up to an hour at a time within 24 hours of its creation, saving the new token it returns. A name you choose with `--name` is issued to anyone who asks for it too, so leave it out when the mail should reach you alone.',
25788
25819
  flags: [TEMP_FLAGS.NAME, TEMP_FLAGS.DOMAIN, TEMP_FLAGS.TTL],
25789
25820
  examples: [
25790
25821
  { command: 'openemail temp new' },
@@ -25821,7 +25852,7 @@ const TempNew = {
25821
25852
  }
25822
25853
  };
25823
25854
 
25824
- const PrintDetail = (context, detail, raw) => {
25855
+ const PrintDetail = (detail, raw) => {
25825
25856
  const view = MessageView(detail.message);
25826
25857
  PrintMailMessage({
25827
25858
  ...view,
@@ -25831,8 +25862,6 @@ const PrintDetail = (context, detail, raw) => {
25831
25862
  subject: view.subject || detail.subject,
25832
25863
  date: view.date ?? detail.receivedAt
25833
25864
  }, raw);
25834
- if (detail.truncated)
25835
- context.io.Warn(TEMP_MESSAGES.TRUNCATED);
25836
25865
  };
25837
25866
  const ListMessages = async (context, target) => {
25838
25867
  const baseUrl = TempBaseUrl(context, target);
@@ -25871,22 +25900,23 @@ const TempRead = {
25871
25900
  aliases: ['show', 'open'],
25872
25901
  summary: 'List the mail in a disposable inbox, or read one message',
25873
25902
  usage: ['temp read [inbox] [message-id] [flags]'],
25874
- description: 'Without a message id it lists what has arrived, newest first, with a short preview that is often enough to read a one time code. With one it prints the whole message, turning HTML into readable text. The inbox is its id or address, and can be left out when you have only one. Reading a message marks it seen. The sender of mail in a temporary inbox is never verified.',
25903
+ description: 'Without a message id it lists what has arrived, newest first, with a short preview that is often enough to read a one time code. With one it prints the whole message, turning HTML into readable text, and lists its attachments by name and size, without their bytes. The inbox is its id or address, and can be left out when you have only one, so a lone `thr_` id reads that message. Reading a message does not mark it seen, and its body is never cut. The sender of mail in a temporary inbox is never verified.',
25875
25904
  arguments: [
25876
25905
  { name: 'inbox', description: 'Inbox id or address from `openemail temp list`' },
25877
- { name: 'message-id', description: 'A `tmail_` id from the list' }
25906
+ { name: 'message-id', description: 'A `thr_` id from the list' }
25878
25907
  ],
25879
25908
  flags: [TEMP_FLAGS.HTML, TEMP_FLAGS.INBOX_TOKEN],
25880
25909
  examples: [
25881
25910
  { command: 'openemail temp read' },
25882
25911
  { command: 'openemail temp read quiet-otter-12@example-temp.com' },
25883
- { command: 'openemail temp read tinb_4f2a tmail_91c3' },
25884
- { command: 'openemail temp read tinb_4f2a tmail_91c3 --html > message.html' }
25912
+ { command: 'openemail temp read thr_9e3b7c1a5f2d8e40b6a9c3f1', note: 'Reads one message from your only inbox' },
25913
+ { command: 'openemail temp read tinb_k7m2q9xw4bdp thr_9e3b7c1a5f2d8e40b6a9c3f1' },
25914
+ { command: 'openemail temp read tinb_k7m2q9xw4bdp thr_9e3b7c1a5f2d8e40b6a9c3f1 --html > message.html' }
25885
25915
  ],
25886
25916
  auth: COMMAND_AUTHS.NONE,
25887
25917
  run: async (context) => {
25888
25918
  const [first, second] = context.args.positional;
25889
- const firstIsMessage = first !== undefined && second === undefined && first.startsWith(TEMP_MESSAGE_PREFIX);
25919
+ const firstIsMessage = first !== undefined && second === undefined && first.startsWith(TEMP_MESSAGE_PREFIX) && !first.includes('@');
25890
25920
  const target = await ResolveTempInbox(context, firstIsMessage ? null : first ?? null);
25891
25921
  const messageId = firstIsMessage ? first : second;
25892
25922
  if (messageId === undefined)
@@ -25896,7 +25926,7 @@ const TempRead = {
25896
25926
  if (context.io.json)
25897
25927
  context.io.Json(detail);
25898
25928
  else
25899
- PrintDetail(context, detail, Flags$2.Bool(context.args.flags, TEMP_FLAG_NAMES.HTML));
25929
+ PrintDetail(detail, Flags$2.Bool(context.args.flags, TEMP_FLAG_NAMES.HTML));
25900
25930
  return EXIT_CODES.OK;
25901
25931
  }
25902
25932
  };
@@ -25925,11 +25955,18 @@ const Print = (context, messages) => {
25925
25955
  };
25926
25956
  const Left = (expiresAt) => RelativeTime(expiresAt).replace(/^in /, '');
25927
25957
  const Waiting = (context, expiresAt) => context.io.Spinner(TEMP_MESSAGES.WAITING(Left(expiresAt)));
25958
+ const Extended = (target, expiresAt) => {
25959
+ if (!target.fromStore)
25960
+ return null;
25961
+ const stored = TempMailStore.Find(target.id);
25962
+ const later = Date.parse(stored?.expiresAt ?? '');
25963
+ return stored !== null && !Number.isNaN(later) && (Number.isNaN(expiresAt) || later > expiresAt) ? stored : null;
25964
+ };
25928
25965
  const TempWatch = {
25929
25966
  name: TEMP_COMMAND_NAMES.WATCH,
25930
25967
  summary: 'Wait for mail to arrive in a disposable inbox',
25931
25968
  usage: ['temp watch [inbox] [--first]'],
25932
- description: 'Prints what is already in the inbox, then checks every 3 seconds and prints each new message as it lands, until you press Ctrl+C or the inbox expires. With `--first` it stops as soon as there is a message, which suits a script waiting for a sign-up code. With `--json` each message is one JSON line on stdout.',
25969
+ description: 'Prints what is already in the inbox, then checks every 3 seconds and prints each new message as it lands, until you press Ctrl+C or the inbox expires. When `openemail temp-mail extend` saves a new token for the inbox meanwhile, the watch carries on with it until the later expiry. With `--first` it stops as soon as there is a message, which suits a script waiting for a sign-up code. With `--json` each message is one JSON line on stdout.',
25933
25970
  arguments: [{ name: 'inbox', description: 'Inbox id or address from `openemail temp list`, optional when you have only one' }],
25934
25971
  flags: [TEMP_FLAGS.FIRST, TEMP_FLAGS.INBOX_TOKEN],
25935
25972
  examples: [
@@ -25942,7 +25979,8 @@ const TempWatch = {
25942
25979
  const target = await ResolveTempInbox(context, context.args.positional[0] ?? null);
25943
25980
  const first = Flags$2.Bool(context.args.flags, TEMP_FLAG_NAMES.FIRST);
25944
25981
  const baseUrl = TempBaseUrl(context, target);
25945
- const List = () => CallAnonymous(context, TEMP_MESSAGES.LOADING, signal => TempClient(baseUrl, signal).listMessages(target.id, { inboxToken: target.token, signal }), { spinner: false });
25982
+ const lease = { token: target.token };
25983
+ const List = () => CallAnonymous(context, TEMP_MESSAGES.LOADING, signal => TempClient(baseUrl, signal).listMessages(target.id, { inboxToken: lease.token, signal }), { spinner: false });
25946
25984
  const opening = await List();
25947
25985
  const seen = new Set(opening.items.map(message => message.id));
25948
25986
  let expiresAt = Date.parse(opening.expiresAt ?? target.expiresAt ?? '');
@@ -25959,6 +25997,12 @@ const TempWatch = {
25959
25997
  const spinner = Waiting(context, expiresAt);
25960
25998
  try {
25961
25999
  for (;;) {
26000
+ const extended = Extended(target, expiresAt);
26001
+ if (extended !== null) {
26002
+ lease.token = extended.inboxToken;
26003
+ expiresAt = Date.parse(extended.expiresAt);
26004
+ spinner.Update(TEMP_MESSAGES.WAITING(Left(expiresAt)));
26005
+ }
25962
26006
  if (!Number.isNaN(expiresAt) && Date.now() >= expiresAt) {
25963
26007
  spinner.Stop();
25964
26008
  context.io.Info(TEMP_MESSAGES.EXPIRED);
@@ -25966,10 +26010,12 @@ const TempWatch = {
25966
26010
  }
25967
26011
  await Sleep$2(TEMP_LIMITS.WATCH_INTERVAL_MS, context.signal);
25968
26012
  const page = await List().catch((error) => {
25969
- if (error instanceof OpenEmailApiError && error.isNotFound)
26013
+ if (InboxGone(error))
25970
26014
  return null;
25971
26015
  throw error;
25972
26016
  });
26017
+ if (page === null && Extended(target, expiresAt) !== null)
26018
+ continue;
25973
26019
  if (page === null) {
25974
26020
  spinner.Stop();
25975
26021
  context.io.Info(TEMP_MESSAGES.EXPIRED);
@@ -25997,12 +26043,12 @@ const Temp = {
25997
26043
  name: TEMP_COMMAND_NAMES.TEMP,
25998
26044
  summary: 'Disposable inboxes for sign-ups and tests',
25999
26045
  usage: ['temp <new|list|read|watch|delete> [arguments] [flags]'],
26000
- description: 'Creates short-lived addresses that anyone can use without signing in, and reads the mail they receive. Each inbox token is kept in `~/.openemail/temp-mail.json` (mode 0600), so later commands find the inbox by its id or address. Mail that lands in a disposable inbox comes from strangers and its sender is never verified.',
26046
+ description: 'Creates short-lived addresses that anyone can use without signing in, and reads the mail they receive. The inbox token is the lease itself and nothing about it is stored on the server, so it is kept in `~/.openemail/temp-mail.json` (mode 0600) and later commands find the inbox by its id or address. A lost token cannot be recovered. Mail that lands in a disposable inbox comes from strangers and its sender is never verified.',
26001
26047
  subcommands: [TempNew, TempList, TempRead, TempWatch, TempDelete],
26002
26048
  examples: [
26003
26049
  { command: 'openemail temp new', note: 'Prints a fresh address' },
26004
26050
  { command: 'openemail temp watch', note: 'Waits for mail and prints each message as it lands' },
26005
- { command: 'openemail temp read tmail_91c3' },
26051
+ { command: 'openemail temp read thr_9e3b7c1a5f2d8e40b6a9c3f1', note: 'Reads one message by its thread id' },
26006
26052
  { command: 'openemail temp delete --yes' }
26007
26053
  ],
26008
26054
  section: HELP_SECTIONS.MAIL,
@@ -26329,6 +26375,13 @@ const RememberRotatedKey = (context, verb, call, value) => {
26329
26375
  };
26330
26376
 
26331
26377
  const Text = (value) => typeof value === 'string' && value.length > 0 ? value : null;
26378
+ const Renew = (id, address, token, expiresAt, baseUrl) => {
26379
+ const stored = TempMailStore.Find(id);
26380
+ if (stored === null || OriginOf(stored.baseUrl ?? DEFAULT_API_URL) !== OriginOf(baseUrl))
26381
+ return;
26382
+ if (TempMailStore.Renew(id, token, expiresAt))
26383
+ Console.Info(RESOURCE_MESSAGES.INBOX_RENEWED(Color.Sanitize(address)));
26384
+ };
26332
26385
  const RememberTempInbox = (verb, call, value, baseUrl) => {
26333
26386
  if (verb.namespace !== RESOURCE_TEMP_INBOX.NAMESPACE)
26334
26387
  return;
@@ -26338,7 +26391,9 @@ const RememberTempInbox = (verb, call, value, baseUrl) => {
26338
26391
  TempMailStore.Remove(inbox);
26339
26392
  return;
26340
26393
  }
26341
- if (call.method !== RESOURCE_TEMP_INBOX.CREATE || !IsPlainObject(value))
26394
+ if (call.method !== RESOURCE_TEMP_INBOX.CREATE && call.method !== RESOURCE_TEMP_INBOX.EXTEND)
26395
+ return;
26396
+ if (!IsPlainObject(value))
26342
26397
  return;
26343
26398
  const id = Text(value.id);
26344
26399
  const address = Text(value.address);
@@ -26346,6 +26401,10 @@ const RememberTempInbox = (verb, call, value, baseUrl) => {
26346
26401
  const expiresAt = Text(value.expiresAt);
26347
26402
  if (id === null || address === null || token === null || expiresAt === null)
26348
26403
  return;
26404
+ if (call.method === RESOURCE_TEMP_INBOX.EXTEND) {
26405
+ Renew(id, address, token, expiresAt, baseUrl);
26406
+ return;
26407
+ }
26349
26408
  TempMailStore.Add({ id, address, inboxToken: token, expiresAt, baseUrl });
26350
26409
  Console.Info(RESOURCE_MESSAGES.INBOX_SAVED(Color.Sanitize(address)));
26351
26410
  };
@@ -26578,7 +26637,7 @@ const RunVerb = (namespace, verb) => async (context) => {
26578
26637
 
26579
26638
  const Code = (text) => `${CODE_SPAN_DELIMITER}${text}${CODE_SPAN_DELIMITER}`;
26580
26639
  const VerbProse = (verb, text) => {
26581
- let prose = text;
26640
+ let prose = text.replace(RESOURCE_EMPTY_STRING_CLEAR, '$1');
26582
26641
  for (const flag of verb.flags) {
26583
26642
  if (flag.parameter === null)
26584
26643
  continue;
@@ -26609,7 +26668,10 @@ const VerbArguments = (verb) => verb.positionals.map(positional => ({
26609
26668
 
26610
26669
  const VerbDescription = (verb) => {
26611
26670
  const prose = VerbProse(verb, verb.entry.description).trim();
26612
- return verb.paginates ? `${prose}\n\n${RESOURCE_HELP.PAGING}` : prose;
26671
+ if (!verb.paginates)
26672
+ return prose;
26673
+ const paging = verb.returns.layout === MANIFEST_DEFAULT_PAGE_LAYOUT ? RESOURCE_HELP.PAGING : RESOURCE_HELP.PAGING_OWN_SHAPE;
26674
+ return `${prose}\n\n${paging}`;
26613
26675
  };
26614
26676
 
26615
26677
  const Collect = (fields, example, pattern, value) => {
@@ -26706,9 +26768,10 @@ const VerbExamples = (namespace, verb, known) => {
26706
26768
  })
26707
26769
  .slice(0, RESOURCE_EXAMPLE_OPTIONAL_FLAGS)
26708
26770
  .flat();
26709
- const examples = [{ command: Join(essential) }];
26771
+ const emptyPatch = verb.entry.method.toUpperCase() === RESOURCE_PATCH_METHOD && required.length === 0 && optional.length > 0;
26772
+ const examples = emptyPatch ? [] : [{ command: Join(essential) }];
26710
26773
  if (optional.length > 0) {
26711
- if (required.length > 0 || verb.positionals.some(positional => positional.required))
26774
+ if (!emptyPatch && (required.length > 0 || verb.positionals.some(positional => positional.required)))
26712
26775
  examples[0].note = RESOURCE_EXAMPLE_NOTES.REQUIRED;
26713
26776
  examples.push({ command: Join([...essential, ...optional]), note: RESOURCE_EXAMPLE_NOTES.FULL });
26714
26777
  }
@@ -26729,7 +26792,7 @@ const VerbExamples = (namespace, verb, known) => {
26729
26792
  }
26730
26793
  const reads = verb.entry.method.toUpperCase() === RESOURCE_READ_METHOD && verb.returns.shape === MANIFEST_RETURN_SHAPES.OBJECT;
26731
26794
  if (examples.length < 2 || reads) {
26732
- examples.push({ command: Join([...essential, `--${GLOBAL_FLAG_NAMES.JSON}`]), note: RESOURCE_EXAMPLE_NOTES.JSON });
26795
+ examples.push({ command: Join([...essential, ...(emptyPatch ? optional : []), `--${GLOBAL_FLAG_NAMES.JSON}`]), note: RESOURCE_EXAMPLE_NOTES.JSON });
26733
26796
  }
26734
26797
  return examples.slice(0, RESOURCE_EXAMPLE_LIMIT);
26735
26798
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openemail/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "author": "OpenEmail",
5
5
  "license": "UNLICENSED",
6
6
  "description": "The official OpenEmail command line. Sign in once, then send and read mail, manage domains, addresses, keys, webhooks, templates, contacts, audiences and every other OpenEmail resource, use the AI tools and connect MCP clients from your terminal.",