@openemail/cli 0.0.2 → 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 +131 -78
  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
@@ -1112,7 +1112,7 @@ const BOOLEAN_TRUE_WORDS = new Set(['true', '1', 'yes', 'on']);
1112
1112
  const BOOLEAN_FALSE_WORDS = new Set(['false', '0', 'no', 'off']);
1113
1113
  const NEGATIVE_NUMBER_PATTERN = /^-\d+(\.\d+)?$/;
1114
1114
 
1115
- var version$1 = "0.0.2";
1115
+ var version$1 = "0.0.3";
1116
1116
 
1117
1117
  const VERSION$1 = version$1;
1118
1118
  const CLI_NAME = 'openemail';
@@ -1245,7 +1245,7 @@ const SCRIPTING_ROWS = [
1245
1245
  const HELP_JSON_SCHEMA_VERSION = 1;
1246
1246
  const CLI_NAME_WORD_PATTERN = new RegExp(`(^|[^\\w-])${CLI_NAME}($|[^\\w-])`);
1247
1247
 
1248
- var version = "0.0.6";
1248
+ var version = "0.0.7";
1249
1249
 
1250
1250
  const BUILD_BASE_URL = 'https://api.openemail.uk';
1251
1251
 
@@ -7501,14 +7501,14 @@ const TempMailCreateMethod = {
7501
7501
  slug: 'temp-mail/create',
7502
7502
  signature: 'create(body?: TempInboxCreate, options?: InboxScope): Promise<CreatedTempInboxResource>',
7503
7503
  summary: 'Create a disposable inbox and its access token',
7504
- 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.',
7505
7505
  method: 'POST',
7506
7506
  path: '/temp-mail/inboxes',
7507
7507
  scopes: [],
7508
7508
  auth: 'none',
7509
7509
  parameters: [
7510
- { 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.' },
7511
- { 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.' },
7512
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.' },
7513
7513
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7514
7514
  { name: 'options.inboxToken', type: 'string', required: false, description: 'Ignored and never sent. Creating an inbox is anonymous and hands you the token instead.' }
@@ -7517,8 +7517,8 @@ const TempMailCreateMethod = {
7517
7517
  example: 'const temp = createTempMail()\n\nconst inbox = await temp.create({ ttlMinutes: 120 })\n\nconsole.log(inbox.address, inbox.expiresAt)\nconsole.log(inbox.token)',
7518
7518
  notes: [
7519
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.',
7520
- '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`.',
7521
- '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.',
7522
7522
  'Unknown body keys are 422 `invalid_parameter`, while a body that is not valid JSON is treated as an empty one.'
7523
7523
  ]
7524
7524
  };
@@ -7530,21 +7530,21 @@ const TempMailDeleteMessageMethod = {
7530
7530
  slug: 'temp-mail/deleteMessage',
7531
7531
  signature: 'deleteMessage(inboxId: string, messageId: string, options?: InboxScope): Promise<DeletedTempMessageResource>',
7532
7532
  summary: 'Delete one message from a disposable inbox',
7533
- 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.',
7534
7534
  method: 'DELETE',
7535
7535
  path: '/temp-mail/inboxes/{id}/messages/{messageId}',
7536
7536
  scopes: [],
7537
7537
  auth: 'inboxToken',
7538
7538
  parameters: [
7539
- { 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.' },
7540
- { 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.' },
7541
7541
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7542
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.' }
7543
7543
  ],
7544
7544
  returns: '`DeletedTempMessageResource` with `object` set to `temp_message`, the message `id` and `deleted: true`.',
7545
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}',
7546
7546
  notes: [
7547
- '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`.',
7548
7548
  'Not retried automatically. If you repeat it yourself after a lost response, that 404 means the first attempt already worked.'
7549
7549
  ]
7550
7550
  };
@@ -7555,14 +7555,14 @@ const TempMailDeleteMethod = {
7555
7555
  accessor: 'openemail.tempMail.delete',
7556
7556
  slug: 'temp-mail/delete',
7557
7557
  signature: 'delete(inboxId: string, options?: InboxScope): Promise<DeletedTempInboxResource>',
7558
- summary: 'Destroy a disposable inbox and its mail now',
7559
- 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.',
7560
7560
  method: 'DELETE',
7561
7561
  path: '/temp-mail/inboxes/{id}',
7562
7562
  scopes: [],
7563
7563
  auth: 'inboxToken',
7564
7564
  parameters: [
7565
- { 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.' },
7566
7566
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7567
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.' }
7568
7568
  ],
@@ -7570,7 +7570,7 @@ const TempMailDeleteMethod = {
7570
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)',
7571
7571
  notes: [
7572
7572
  'The flag is `destroyed`, not `deleted` as on other tombstones.',
7573
- '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.'
7574
7574
  ]
7575
7575
  };
7576
7576
 
@@ -7579,23 +7579,23 @@ const TempMailExtendMethod = {
7579
7579
  name: 'extend',
7580
7580
  accessor: 'openemail.tempMail.extend',
7581
7581
  slug: 'temp-mail/extend',
7582
- signature: 'extend(inboxId: string, options?: InboxScope): Promise<TempInboxResource>',
7582
+ signature: 'extend(inboxId: string, options?: InboxScope): Promise<ExtendedTempInboxResource>',
7583
7583
  summary: 'Push the expiry of an inbox an hour further out',
7584
- 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.',
7585
7585
  method: 'POST',
7586
7586
  path: '/temp-mail/inboxes/{id}/extend',
7587
7587
  scopes: [],
7588
7588
  auth: 'inboxToken',
7589
7589
  parameters: [
7590
- { 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.' },
7591
7591
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7592
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.' }
7593
7593
  ],
7594
- returns: '`TempInboxResource` with the new `expiresAt` and the updated `extensionsLeft`.',
7595
- 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 })',
7596
7596
  notes: [
7597
7597
  'Not retried automatically, because a replay would spend a second extension.',
7598
- '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.'
7599
7599
  ]
7600
7600
  };
7601
7601
 
@@ -7606,21 +7606,21 @@ const TempMailGetMessageMethod = {
7606
7606
  slug: 'temp-mail/getMessage',
7607
7607
  signature: 'getMessage(inboxId: string, messageId: string, options?: InboxScope): Promise<TempMessageDetailResource>',
7608
7608
  summary: 'Read one message with its stored body',
7609
- 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.',
7610
7610
  method: 'GET',
7611
7611
  path: '/temp-mail/inboxes/{id}/messages/{messageId}',
7612
7612
  scopes: [],
7613
7613
  auth: 'inboxToken',
7614
7614
  parameters: [
7615
- { 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.' },
7616
- { 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.' },
7617
7617
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7618
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.' }
7619
7619
  ],
7620
7620
  returns: '`TempMessageDetailResource`: every `TempMessageResource` field plus `message`, the stored parsed message, and `truncated`.',
7621
- 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)',
7622
7622
  notes: [
7623
- '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`.',
7624
7624
  '`message` is typed as a loose record, so narrow `decodedBody` to a string before rendering it.'
7625
7625
  ]
7626
7626
  };
@@ -7632,20 +7632,20 @@ const TempMailGetMethod = {
7632
7632
  slug: 'temp-mail/get',
7633
7633
  signature: 'get(inboxId: string, options?: InboxScope): Promise<TempInboxResource>',
7634
7634
  summary: 'Read the lease and counters of a disposable inbox',
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 `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.',
7636
7636
  method: 'GET',
7637
7637
  path: '/temp-mail/inboxes/{id}',
7638
7638
  scopes: [],
7639
7639
  auth: 'inboxToken',
7640
7640
  parameters: [
7641
- { 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.' },
7642
7642
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7643
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.' }
7644
7644
  ],
7645
7645
  returns: '`TempInboxResource` with `id`, `address`, `domain`, `createdAt`, `expiresAt`, `extensionsLeft`, `messageCount`, `messageLimit` and `lastMessageAt`.',
7646
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)',
7647
7647
  notes: [
7648
- '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`.',
7649
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.'
7650
7650
  ]
7651
7651
  };
@@ -7709,23 +7709,23 @@ const TempMailListAttachmentsMethod = {
7709
7709
  name: 'listAttachments',
7710
7710
  accessor: 'openemail.tempMail.listAttachments',
7711
7711
  slug: 'temp-mail/listAttachments',
7712
- signature: 'listAttachments(inboxId: string, messageId: string, options?: InboxScope): Promise<Array<AttachmentResource>>',
7713
- summary: 'Read the attachments of a message with their bytes',
7714
- 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`.',
7715
7715
  method: 'GET',
7716
7716
  path: '/temp-mail/inboxes/{id}/messages/{messageId}/attachments',
7717
7717
  scopes: [],
7718
7718
  auth: 'inboxToken',
7719
7719
  parameters: [
7720
- { 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.' },
7721
- { 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.' },
7722
7722
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
7723
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.' }
7724
7724
  ],
7725
- returns: '`Array<AttachmentResource>`, each with `attachmentId`, `filename`, `contentType`, `size` in decoded bytes and `content` as base64 or null.',
7726
- 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)',
7727
7727
  notes: [
7728
- 'Reading attachments marks the message seen, the same as `getMessage`.',
7728
+ 'Listing attachments does not mark the message seen.',
7729
7729
  'Check `attachmentCount` from `listMessages` first to skip this call for messages with none.'
7730
7730
  ]
7731
7731
  };
@@ -7737,7 +7737,7 @@ const TempMailListDomainsMethod = {
7737
7737
  slug: 'temp-mail/listDomains',
7738
7738
  signature: 'listDomains(options?: InboxScope): Promise<Array<TempDomainResource>>',
7739
7739
  summary: 'List the domains a disposable inbox can be created on',
7740
- 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`.',
7741
7741
  method: 'GET',
7742
7742
  path: '/temp-mail/domains',
7743
7743
  scopes: [],
@@ -7749,7 +7749,7 @@ const TempMailListDomainsMethod = {
7749
7749
  returns: '`Array<TempDomainResource>`, each with `object` set to `temp_domain` and a lowercased `domain` to pass back to `create`.',
7750
7750
  example: 'const temp = createTempMail()\n\nconst domains = await temp.listDomains()\n\nconsole.log(domains.map((entry) => entry.domain))',
7751
7751
  notes: [
7752
- '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.',
7753
7753
  'Retried automatically on network failure and retryable statuses, like every GET.'
7754
7754
  ]
7755
7755
  };
@@ -7761,13 +7761,13 @@ const TempMailListMessagesMethod = {
7761
7761
  slug: 'temp-mail/listMessages',
7762
7762
  signature: 'listMessages(inboxId: string, options?: TempMessageListOptions): Promise<TempMessagesResource>',
7763
7763
  summary: 'List one page of the messages in a disposable inbox',
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 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.',
7765
7765
  method: 'GET',
7766
7766
  path: '/temp-mail/inboxes/{id}/messages',
7767
7767
  scopes: [],
7768
7768
  auth: 'inboxToken',
7769
7769
  parameters: [
7770
- { 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.' },
7771
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`.' },
7772
7772
  { name: 'options.cursor', type: 'string', required: false, description: 'The `nextCursor` from the previous page. Never build one yourself.' },
7773
7773
  { name: 'options.signal', type: 'AbortSignal', required: false, description: 'Cancels the request.' },
@@ -7779,7 +7779,8 @@ const TempMailListMessagesMethod = {
7779
7779
  notes: [
7780
7780
  'A message whose `Message-ID` header matches one already in the inbox is not stored twice.',
7781
7781
  '`to` is the inbox address. A `+tag` the sender added is folded back into the base address.',
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.'
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`.'
7783
7784
  ]
7784
7785
  };
7785
7786
 
@@ -13662,6 +13663,7 @@ const RESOURCE_ONE_TIME_SECRETS = {
13662
13663
  const RESOURCE_TEMP_INBOX = {
13663
13664
  NAMESPACE: 'tempMail',
13664
13665
  CREATE: 'create',
13666
+ EXTEND: 'extend',
13665
13667
  DELETE: 'delete',
13666
13668
  TOKEN_FIELD: 'token'
13667
13669
  };
@@ -13819,6 +13821,12 @@ const RESOURCE_TYPE_COLUMNS = {
13819
13821
  TempMessagesResource: TEMP_MESSAGE_COLUMNS,
13820
13822
  TempMessageResource: TEMP_MESSAGE_COLUMNS,
13821
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
+ ],
13822
13830
  ApiKeyRequestResource: [
13823
13831
  Column('id', 'id', RENDER_CELL_FORMATS.ID),
13824
13832
  Column('method', 'method'),
@@ -13978,7 +13986,7 @@ const RESOURCE_HELP = {
13978
13986
  NOTES: 'NOTES',
13979
13987
  NO_SCOPES: 'Any key or sign-in for the workspace.',
13980
13988
  SIGNED_OUT: 'None. It works signed out, with no key.',
13981
- 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.',
13982
13990
  REQUIRED: 'Required.',
13983
13991
  REQUIRED_WITH_DATA: 'Required, here or in `--data`.',
13984
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.',
@@ -14008,6 +14016,7 @@ const RESOURCE_MESSAGES = {
14008
14016
  INBOX_TOKEN_REJECTED: 'The inbox token was refused.',
14009
14017
  INBOX_TOKEN_REJECTED_NEXT: 'Check `--inbox-token`, or create a new inbox with `openemail temp new`.',
14010
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.`,
14011
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.`,
14012
14021
  NO_KEYLESS_RESOURCE: (namespace) => `The ${namespace} commands need a credential.`,
14013
14022
  NO_METHOD: (accessor) => `${accessor} is not in the SDK this CLI was built with.`,
@@ -14108,13 +14117,13 @@ const TEMP_FLAGS = {
14108
14117
  name: TEMP_FLAG_NAMES.DOMAIN,
14109
14118
  kind: FLAG_KINDS.STRING,
14110
14119
  placeholder: 'domain',
14111
- 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'
14112
14121
  },
14113
14122
  NAME: {
14114
14123
  name: TEMP_FLAG_NAMES.NAME,
14115
14124
  kind: FLAG_KINDS.STRING,
14116
14125
  placeholder: 'local-part',
14117
- 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'
14118
14127
  },
14119
14128
  TTL: {
14120
14129
  name: TEMP_FLAG_NAMES.TTL,
@@ -14146,7 +14155,10 @@ const TEMP_LIMITS = {
14146
14155
  SNIPPET_WIDTH: 60
14147
14156
  };
14148
14157
  const TEMP_TOKEN_PREFIX = 'oe_inbox_';
14149
- const TEMP_MESSAGE_PREFIX = 'tmail_';
14158
+ const TEMP_MESSAGE_PREFIX = 'thr_';
14159
+ const TEMP_API_ERROR_CODES = {
14160
+ INBOX_EXPIRED: 'inbox_expired'
14161
+ };
14150
14162
  const TEMP_COLUMNS = {
14151
14163
  ID: 'Inbox',
14152
14164
  ADDRESS: 'Address',
@@ -14161,7 +14173,7 @@ const TEMP_MESSAGES = {
14161
14173
  CREATING: 'Creating a temporary inbox',
14162
14174
  LOADING: 'Loading messages',
14163
14175
  LOADING_MESSAGE: 'Loading the message',
14164
- DELETING: 'Deleting the inbox',
14176
+ DELETING: 'Moving the mail to the bin',
14165
14177
  NO_INBOXES: 'You have no temporary inboxes.',
14166
14178
  NO_INBOXES_NEXT: 'Run `openemail temp new` to create one.',
14167
14179
  PICK_INBOX: 'Which inbox?',
@@ -14170,12 +14182,9 @@ const TEMP_MESSAGES = {
14170
14182
  TTL_RANGE: '--ttl takes a whole number of minutes from 1 to 1440.',
14171
14183
  INVALID_TOKEN: '--inbox-token must be an inbox token beginning oe_inbox_.',
14172
14184
  NO_MESSAGES: 'No mail yet.',
14173
- WATCH_STOPPED: 'Stopped watching.',
14174
14185
  EXPIRED: 'The inbox has expired, so there is nothing more to watch.',
14175
- GONE_LOCALLY: 'The inbox was already gone on the server, so it was only forgotten here.',
14176
- TRUNCATED: 'The body is longer than 2 MB, so only the first 2 MB is shown.',
14177
- EMPTY_BODY: '(This message has no body.)',
14178
- 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.',
14179
14188
  MORE: (id) => `Older messages are not shown here. Run \`openemail temp-mail list-messages ${id} --all\` to see every one.`,
14180
14189
  ALREADY_HERE: 'Already in the inbox:',
14181
14190
  CREATED: (address) => `Created ${address}.`,
@@ -14185,8 +14194,8 @@ const TEMP_MESSAGES = {
14185
14194
  PRUNED: (count) => `Forgot ${count} expired ${count === 1 ? 'inbox' : 'inboxes'}.`,
14186
14195
  WATCHING: (address) => `Watching ${address}`,
14187
14196
  WAITING: (left) => `Waiting for mail, ${left} left. Press Ctrl+C to stop`,
14188
- CONFIRM_DELETE: (address) => `Delete ${address} and every message in it? This cannot be undone.`,
14189
- 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.`,
14190
14199
  SENDER_HINT: (id) => `Run \`openemail temp read ${id} <message-id>\` to read one.`
14191
14200
  };
14192
14201
 
@@ -18420,6 +18429,17 @@ const Find$1 = (idOrAddress) => {
18420
18429
  return List$2().find(inbox => inbox.id === idOrAddress || inbox.address.toLowerCase() === needle) ?? null;
18421
18430
  };
18422
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
+ };
18423
18443
  const Remove$1 = (id) => {
18424
18444
  const outcome = { removed: false };
18425
18445
  Change(inboxes => {
@@ -18443,6 +18463,7 @@ const TempMailStore = {
18443
18463
  List: List$2,
18444
18464
  Find: Find$1,
18445
18465
  Add,
18466
+ Renew: Renew$1,
18446
18467
  Remove: Remove$1,
18447
18468
  PruneExpired
18448
18469
  };
@@ -25637,6 +25658,8 @@ const Send = {
25637
25658
  }
25638
25659
  };
25639
25660
 
25661
+ const InboxGone = (error) => error instanceof OpenEmailApiError && (error.isNotFound || error.code === TEMP_API_ERROR_CODES.INBOX_EXPIRED);
25662
+
25640
25663
  const FromStored = (inbox) => ({
25641
25664
  id: inbox.id,
25642
25665
  address: inbox.address,
@@ -25703,15 +25726,15 @@ const TempClient = (baseUrl, signal) => createTempMail(SdkBaseOptions(baseUrl, s
25703
25726
  const TempDelete = {
25704
25727
  name: TEMP_COMMAND_NAMES.DELETE,
25705
25728
  aliases: ['rm'],
25706
- summary: 'Delete a disposable inbox and forget it',
25729
+ summary: 'Move the mail in a disposable inbox to the bin and forget its token',
25707
25730
  usage: ['temp delete [inbox] [--yes]'],
25708
- 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`.',
25709
25732
  arguments: [{ name: 'inbox', description: 'Inbox id or address from `openemail temp list`, optional when you have only one' }],
25710
25733
  flags: [TEMP_FLAGS.INBOX_TOKEN],
25711
25734
  examples: [
25712
25735
  { command: 'openemail temp delete' },
25713
25736
  { command: 'openemail temp rm quiet-otter-12@example-temp.com --yes' },
25714
- { command: 'openemail temp delete tinb_4f2a --yes --json' }
25737
+ { command: 'openemail temp delete tinb_k7m2q9xw4bdp --yes --json' }
25715
25738
  ],
25716
25739
  auth: COMMAND_AUTHS.NONE,
25717
25740
  destructive: true,
@@ -25722,7 +25745,7 @@ const TempDelete = {
25722
25745
  if (!await ConfirmDestructive(TEMP_MESSAGES.CONFIRM_DELETE(address)))
25723
25746
  return EXIT_CODES.CANCELLED;
25724
25747
  const gone = await CallAnonymous(context, TEMP_MESSAGES.DELETING, signal => TempClient(baseUrl, signal).delete(target.id, { inboxToken: target.token, signal })).then(() => false, (error) => {
25725
- if (error instanceof OpenEmailApiError && error.isNotFound)
25748
+ if (InboxGone(error))
25726
25749
  return true;
25727
25750
  throw error;
25728
25751
  });
@@ -25731,10 +25754,12 @@ const TempDelete = {
25731
25754
  context.io.Json({ object: 'temp_inbox', id: target.id, address: target.address, destroyed: !gone, forgotten });
25732
25755
  return EXIT_CODES.OK;
25733
25756
  }
25734
- if (gone)
25757
+ if (gone) {
25735
25758
  context.io.Warn(TEMP_MESSAGES.GONE_LOCALLY);
25736
- else
25737
- 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);
25738
25763
  return EXIT_CODES.OK;
25739
25764
  }
25740
25765
  };
@@ -25743,7 +25768,7 @@ const TempList = {
25743
25768
  name: TEMP_COMMAND_NAMES.LIST,
25744
25769
  summary: 'List the disposable inboxes this CLI created',
25745
25770
  usage: ['temp list'],
25746
- 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.',
25747
25772
  examples: [
25748
25773
  { command: 'openemail temp list' },
25749
25774
  { command: 'openemail temp ls --json', note: 'Ids, addresses and expiry times, never the tokens' }
@@ -25790,7 +25815,7 @@ const TempNew = {
25790
25815
  aliases: ['create'],
25791
25816
  summary: 'Create a disposable inbox',
25792
25817
  usage: ['temp new [--name <local-part>] [--domain <domain>] [--ttl <minutes>]'],
25793
- 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.',
25794
25819
  flags: [TEMP_FLAGS.NAME, TEMP_FLAGS.DOMAIN, TEMP_FLAGS.TTL],
25795
25820
  examples: [
25796
25821
  { command: 'openemail temp new' },
@@ -25827,7 +25852,7 @@ const TempNew = {
25827
25852
  }
25828
25853
  };
25829
25854
 
25830
- const PrintDetail = (context, detail, raw) => {
25855
+ const PrintDetail = (detail, raw) => {
25831
25856
  const view = MessageView(detail.message);
25832
25857
  PrintMailMessage({
25833
25858
  ...view,
@@ -25837,8 +25862,6 @@ const PrintDetail = (context, detail, raw) => {
25837
25862
  subject: view.subject || detail.subject,
25838
25863
  date: view.date ?? detail.receivedAt
25839
25864
  }, raw);
25840
- if (detail.truncated)
25841
- context.io.Warn(TEMP_MESSAGES.TRUNCATED);
25842
25865
  };
25843
25866
  const ListMessages = async (context, target) => {
25844
25867
  const baseUrl = TempBaseUrl(context, target);
@@ -25877,22 +25900,23 @@ const TempRead = {
25877
25900
  aliases: ['show', 'open'],
25878
25901
  summary: 'List the mail in a disposable inbox, or read one message',
25879
25902
  usage: ['temp read [inbox] [message-id] [flags]'],
25880
- 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.',
25881
25904
  arguments: [
25882
25905
  { name: 'inbox', description: 'Inbox id or address from `openemail temp list`' },
25883
- { name: 'message-id', description: 'A `tmail_` id from the list' }
25906
+ { name: 'message-id', description: 'A `thr_` id from the list' }
25884
25907
  ],
25885
25908
  flags: [TEMP_FLAGS.HTML, TEMP_FLAGS.INBOX_TOKEN],
25886
25909
  examples: [
25887
25910
  { command: 'openemail temp read' },
25888
25911
  { command: 'openemail temp read quiet-otter-12@example-temp.com' },
25889
- { command: 'openemail temp read tinb_4f2a tmail_91c3' },
25890
- { 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' }
25891
25915
  ],
25892
25916
  auth: COMMAND_AUTHS.NONE,
25893
25917
  run: async (context) => {
25894
25918
  const [first, second] = context.args.positional;
25895
- 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('@');
25896
25920
  const target = await ResolveTempInbox(context, firstIsMessage ? null : first ?? null);
25897
25921
  const messageId = firstIsMessage ? first : second;
25898
25922
  if (messageId === undefined)
@@ -25902,7 +25926,7 @@ const TempRead = {
25902
25926
  if (context.io.json)
25903
25927
  context.io.Json(detail);
25904
25928
  else
25905
- 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));
25906
25930
  return EXIT_CODES.OK;
25907
25931
  }
25908
25932
  };
@@ -25931,11 +25955,18 @@ const Print = (context, messages) => {
25931
25955
  };
25932
25956
  const Left = (expiresAt) => RelativeTime(expiresAt).replace(/^in /, '');
25933
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
+ };
25934
25965
  const TempWatch = {
25935
25966
  name: TEMP_COMMAND_NAMES.WATCH,
25936
25967
  summary: 'Wait for mail to arrive in a disposable inbox',
25937
25968
  usage: ['temp watch [inbox] [--first]'],
25938
- 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.',
25939
25970
  arguments: [{ name: 'inbox', description: 'Inbox id or address from `openemail temp list`, optional when you have only one' }],
25940
25971
  flags: [TEMP_FLAGS.FIRST, TEMP_FLAGS.INBOX_TOKEN],
25941
25972
  examples: [
@@ -25948,7 +25979,8 @@ const TempWatch = {
25948
25979
  const target = await ResolveTempInbox(context, context.args.positional[0] ?? null);
25949
25980
  const first = Flags$2.Bool(context.args.flags, TEMP_FLAG_NAMES.FIRST);
25950
25981
  const baseUrl = TempBaseUrl(context, target);
25951
- 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 });
25952
25984
  const opening = await List();
25953
25985
  const seen = new Set(opening.items.map(message => message.id));
25954
25986
  let expiresAt = Date.parse(opening.expiresAt ?? target.expiresAt ?? '');
@@ -25965,6 +25997,12 @@ const TempWatch = {
25965
25997
  const spinner = Waiting(context, expiresAt);
25966
25998
  try {
25967
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
+ }
25968
26006
  if (!Number.isNaN(expiresAt) && Date.now() >= expiresAt) {
25969
26007
  spinner.Stop();
25970
26008
  context.io.Info(TEMP_MESSAGES.EXPIRED);
@@ -25972,10 +26010,12 @@ const TempWatch = {
25972
26010
  }
25973
26011
  await Sleep$2(TEMP_LIMITS.WATCH_INTERVAL_MS, context.signal);
25974
26012
  const page = await List().catch((error) => {
25975
- if (error instanceof OpenEmailApiError && error.isNotFound)
26013
+ if (InboxGone(error))
25976
26014
  return null;
25977
26015
  throw error;
25978
26016
  });
26017
+ if (page === null && Extended(target, expiresAt) !== null)
26018
+ continue;
25979
26019
  if (page === null) {
25980
26020
  spinner.Stop();
25981
26021
  context.io.Info(TEMP_MESSAGES.EXPIRED);
@@ -26003,12 +26043,12 @@ const Temp = {
26003
26043
  name: TEMP_COMMAND_NAMES.TEMP,
26004
26044
  summary: 'Disposable inboxes for sign-ups and tests',
26005
26045
  usage: ['temp <new|list|read|watch|delete> [arguments] [flags]'],
26006
- 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.',
26007
26047
  subcommands: [TempNew, TempList, TempRead, TempWatch, TempDelete],
26008
26048
  examples: [
26009
26049
  { command: 'openemail temp new', note: 'Prints a fresh address' },
26010
26050
  { command: 'openemail temp watch', note: 'Waits for mail and prints each message as it lands' },
26011
- { command: 'openemail temp read tmail_91c3' },
26051
+ { command: 'openemail temp read thr_9e3b7c1a5f2d8e40b6a9c3f1', note: 'Reads one message by its thread id' },
26012
26052
  { command: 'openemail temp delete --yes' }
26013
26053
  ],
26014
26054
  section: HELP_SECTIONS.MAIL,
@@ -26335,6 +26375,13 @@ const RememberRotatedKey = (context, verb, call, value) => {
26335
26375
  };
26336
26376
 
26337
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
+ };
26338
26385
  const RememberTempInbox = (verb, call, value, baseUrl) => {
26339
26386
  if (verb.namespace !== RESOURCE_TEMP_INBOX.NAMESPACE)
26340
26387
  return;
@@ -26344,7 +26391,9 @@ const RememberTempInbox = (verb, call, value, baseUrl) => {
26344
26391
  TempMailStore.Remove(inbox);
26345
26392
  return;
26346
26393
  }
26347
- 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))
26348
26397
  return;
26349
26398
  const id = Text(value.id);
26350
26399
  const address = Text(value.address);
@@ -26352,6 +26401,10 @@ const RememberTempInbox = (verb, call, value, baseUrl) => {
26352
26401
  const expiresAt = Text(value.expiresAt);
26353
26402
  if (id === null || address === null || token === null || expiresAt === null)
26354
26403
  return;
26404
+ if (call.method === RESOURCE_TEMP_INBOX.EXTEND) {
26405
+ Renew(id, address, token, expiresAt, baseUrl);
26406
+ return;
26407
+ }
26355
26408
  TempMailStore.Add({ id, address, inboxToken: token, expiresAt, baseUrl });
26356
26409
  Console.Info(RESOURCE_MESSAGES.INBOX_SAVED(Color.Sanitize(address)));
26357
26410
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openemail/cli",
3
- "version": "0.0.2",
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.",