Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
pull_request:
push:
branches:
- master

jobs:
test:
name: Go test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Verify formatting
run: |
fmt_out=$(gofmt -l .)
if [ -n "$fmt_out" ]; then
echo "The following files are not gofmt-formatted:"
echo "$fmt_out"
exit 1
fi

- name: Run vet
run: go vet ./...

- name: Run tests
run: go test ./...
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Response:

```bash
pm-cli mail read 123 --json
pm-cli mail read uid:456 --json
pm-cli mail read 123 --unread --json
```

Response:
Expand Down Expand Up @@ -117,6 +119,7 @@ pm-cli mail flag 123 --star --json

```bash
pm-cli mail move 123 Archive --json
pm-cli mail archive 123 --json
```

### Delete Messages
Expand Down Expand Up @@ -145,7 +148,7 @@ Exit codes:

Messages are identified by sequence number (`seq_num`), which is the ID shown in `mail list`. Use this number for `mail read`, `mail delete`, `mail move`, and `mail flag`.

Note: Sequence numbers can change when messages are deleted. For persistent identification, use the `uid` field.
Note: Sequence numbers can change when messages are deleted. For persistent identification, use the `uid` field and pass IDs as `uid:<uid>` (for example `pm-cli mail read uid:456 --json`).

## Mailbox Names

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ pm-cli mail list --json # JSON output

```bash
pm-cli mail read 123 # Read message #123
pm-cli mail read uid:456 # Read by stable UID
pm-cli mail read 123 -m Archive # Read from a specific mailbox
pm-cli mail read 123 --json # JSON output with body
pm-cli mail read 123 --headers # Include all headers
pm-cli mail read 123 --unread # Mark unread after reading
pm-cli mail read 123 --html # Output HTML body
pm-cli mail read 123 --attachments # List attachments
pm-cli mail read 123 --raw # Raw MIME source
Expand Down Expand Up @@ -106,12 +108,24 @@ pm-cli mail delete 123 456 789 # Batch delete
pm-cli mail delete --query "from:spam@example.com" # Delete by search
pm-cli mail delete 123 --permanent # Delete permanently
pm-cli mail move 123 Archive # Move to folder
pm-cli mail archive 123 # Shortcut: move to Archive
pm-cli mail move 123 456 -d Archive # Batch move
pm-cli mail flag 123 --read # Mark as read
pm-cli mail flag 123 --star # Add star
pm-cli mail flag 123 456 --unread # Batch flag
```

### Message IDs

Use sequence numbers by default (the `ID` shown in `mail list`), or use explicit UID selectors for stable references:

```bash
pm-cli mail read 123 # sequence number (can change over time)
pm-cli mail read uid:456 # UID selector (stable within mailbox)
```

JSON outputs include both `seq_num` and `uid`. `mail read` JSON and header output include `message_id` (RFC 5322 Message-ID header) when available.

### Labels

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pm-cli
│ ├── forward # Forward message
│ ├── delete # Delete messages
│ ├── move # Move to folder
│ ├── archive # Move to Archive
│ ├── flag # Manage flags
│ ├── search # Search messages
│ └── download # Save attachment
Expand Down
8 changes: 8 additions & 0 deletions docs/ai-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pm-cli mailbox list --json
pm-cli config doctor --json
```

Message selectors accept either sequence numbers (`123`) or stable UID selectors (`uid:456`) across read/delete/move/flag/download/thread operations.

## Command Schema

Get the full command schema as JSON:
Expand Down Expand Up @@ -77,6 +79,12 @@ Output:
}
```

### Stable ID Guidance

- `seq_num` is mailbox-local and can change after deletes/expunges.
- `uid` is stable within a mailbox and preferred for persistent workflows.
- Use `uid:<uid>` in command arguments when you need stability.

### Send Email

```bash
Expand Down
25 changes: 25 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ Read a specific message.
pm-cli mail read <id> [flags]
```

`<id>` accepts either a sequence number (for example `123`) or `uid:<uid>` (for example `uid:456`).

**Flags:**
| Flag | Description |
|------|-------------|
Expand All @@ -157,15 +159,18 @@ pm-cli mail read <id> [flags]
| `--headers` | Include all headers |
| `--attachments` | List attachments only |
| `--html` | Output HTML body instead of plain text |
| `--unread` | Mark as unread after reading (remove `\Seen`) |

**Examples:**
```bash
pm-cli mail read 123
pm-cli mail read uid:456
pm-cli mail read 123 -m Archive
pm-cli mail read 123 --headers
pm-cli mail read 123 --raw
pm-cli mail read 123 --html # View HTML content
pm-cli mail read 123 --attachments
pm-cli mail read 123 --unread # Read but keep unread
pm-cli mail read 123 --json
```

Expand Down Expand Up @@ -292,6 +297,8 @@ Delete messages.
pm-cli mail delete <id>... [flags]
```

`<id>` accepts sequence numbers or `uid:<uid>`.

**Flags:**
| Flag | Description |
|------|-------------|
Expand All @@ -312,12 +319,28 @@ Move a message to another mailbox.
pm-cli mail move <id> <mailbox>
```

`<id>` accepts sequence numbers or `uid:<uid>`.

**Examples:**
```bash
pm-cli mail move 123 Archive
pm-cli mail move uid:456 Archive
pm-cli mail move 123 "Projects/Active"
```

To archive quickly:

```bash
pm-cli mail archive <id>...
```

Examples:

```bash
pm-cli mail archive 123
pm-cli mail archive uid:456
```

### mail flag

Manage message flags.
Expand All @@ -326,6 +349,8 @@ Manage message flags.
pm-cli mail flag <id> [flags]
```

`<id>` accepts sequence numbers or `uid:<uid>`.

**Flags:**
| Flag | Description |
|------|-------------|
Expand Down
58 changes: 33 additions & 25 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"github.com/bscott/pm-cli/internal/output"
)

var Version = "0.2.1"
var Version = "0.2.2"

type Globals struct {
JSON bool `help:"Output as JSON" name:"json"`
Expand Down Expand Up @@ -82,31 +82,32 @@ type ConfigDoctorCmd struct{}

// MailCmd handles email operations
type MailCmd struct {
List MailListCmd `cmd:"" help:"List messages in mailbox"`
Read MailReadCmd `cmd:"" help:"Read a specific message"`
Send MailSendCmd `cmd:"" help:"Compose and send email"`
Reply MailReplyCmd `cmd:"" help:"Reply to a message"`
Forward MailForwardCmd `cmd:"" help:"Forward a message"`
Delete MailDeleteCmd `cmd:"" help:"Delete message(s)"`
Move MailMoveCmd `cmd:"" help:"Move message to mailbox"`
Flag MailFlagCmd `cmd:"" help:"Manage message flags"`
Search MailSearchCmd `cmd:"" help:"Search messages"`
Download MailDownloadCmd `cmd:"" help:"Download attachment"`
Draft DraftCmd `cmd:"" help:"Manage drafts"`
Thread MailThreadCmd `cmd:"" help:"Show conversation thread"`
Watch MailWatchCmd `cmd:"" help:"Watch for new messages"`
Label LabelCmd `cmd:"" help:"Manage message labels"`
List MailListCmd `cmd:"" help:"List messages in mailbox"`
Read MailReadCmd `cmd:"" help:"Read a specific message"`
Send MailSendCmd `cmd:"" help:"Compose and send email"`
Reply MailReplyCmd `cmd:"" help:"Reply to a message"`
Forward MailForwardCmd `cmd:"" help:"Forward a message"`
Delete MailDeleteCmd `cmd:"" help:"Delete message(s)"`
Move MailMoveCmd `cmd:"" help:"Move message to mailbox"`
Archive MailArchiveCmd `cmd:"" help:"Move message(s) to Archive"`
Flag MailFlagCmd `cmd:"" help:"Manage message flags"`
Search MailSearchCmd `cmd:"" help:"Search messages"`
Download MailDownloadCmd `cmd:"" help:"Download attachment"`
Draft DraftCmd `cmd:"" help:"Manage drafts"`
Thread MailThreadCmd `cmd:"" help:"Show conversation thread"`
Watch MailWatchCmd `cmd:"" help:"Watch for new messages"`
Label LabelCmd `cmd:"" help:"Manage message labels"`
Summarize MailSummarizeCmd `cmd:"" help:"Summarize message for AI processing"`
Extract MailExtractCmd `cmd:"" help:"Extract structured data from message"`
}

type MailSummarizeCmd struct {
ID string `arg:"" help:"Message ID to summarize"`
ID string `arg:"" help:"Message sequence number or uid:<uid> to summarize"`
Mailbox string `help:"Mailbox name" short:"m" default:"INBOX"`
}

type MailExtractCmd struct {
ID string `arg:"" help:"Message ID to extract data from"`
ID string `arg:"" help:"Message sequence number or uid:<uid> to extract data from"`
Mailbox string `help:"Mailbox name" short:"m" default:"INBOX"`
}

Expand Down Expand Up @@ -139,7 +140,7 @@ type MailWatchCmd struct {
}

type MailThreadCmd struct {
ID string `arg:"" help:"Message ID to show thread for"`
ID string `arg:"" help:"Message sequence number or uid:<uid> to show thread for"`
Mailbox string `help:"Mailbox to search" short:"m" default:"INBOX"`
}

Expand Down Expand Up @@ -185,12 +186,13 @@ type MailListCmd struct {
}

type MailReadCmd struct {
ID string `arg:"" help:"Message ID or sequence number"`
ID string `arg:"" help:"Message sequence number or uid:<uid>"`
Mailbox string `help:"Mailbox name" short:"m"`
Raw bool `help:"Show raw message"`
Headers bool `help:"Include all headers"`
Attachments bool `help:"List attachments"`
HTML bool `help:"Output HTML body instead of plain text"`
Unread bool `help:"Mark as unread after reading (remove \\\\Seen)" name:"unread"`
}

type MailSendCmd struct {
Expand All @@ -206,43 +208,49 @@ type MailSendCmd struct {
}

type MailReplyCmd struct {
ID string `arg:"" help:"Message ID to reply to"`
ID string `arg:"" help:"Message sequence number or uid:<uid> to reply to"`
All bool `help:"Reply to all recipients" name:"all"`
Body string `help:"Reply body" short:"b"`
Attach []string `help:"Attachments" short:"a" type:"existingfile"`
IdempotencyKey string `help:"Unique key to prevent duplicate sends" name:"idempotency-key"`
}

type MailForwardCmd struct {
ID string `arg:"" help:"Message ID to forward"`
ID string `arg:"" help:"Message sequence number or uid:<uid> to forward"`
To []string `help:"Recipient(s)" short:"t" required:""`
Body string `help:"Additional message" short:"b"`
Attach []string `help:"Additional attachments" short:"a" type:"existingfile"`
IdempotencyKey string `help:"Unique key to prevent duplicate sends" name:"idempotency-key"`
}

type MailDeleteCmd struct {
IDs []string `arg:"" optional:"" help:"Message ID(s) to delete"`
IDs []string `arg:"" optional:"" help:"Message sequence number(s) or uid:<uid> to delete"`
Query string `help:"Delete messages matching search query (e.g., 'from:spam@example.com')"`
Mailbox string `help:"Mailbox to operate on" short:"m" default:"INBOX"`
Permanent bool `help:"Skip trash, delete permanently"`
}

type MailDownloadCmd struct {
ID string `arg:"" help:"Message ID"`
ID string `arg:"" help:"Message sequence number or uid:<uid>"`
Index int `arg:"" help:"Attachment index (0-based)"`
Out string `help:"Output path (default: original filename)" short:"o"`
}

type MailMoveCmd struct {
IDs []string `arg:"" optional:"" help:"Message ID(s) to move"`
IDs []string `arg:"" optional:"" help:"Message sequence number(s) or uid:<uid> to move"`
Destination string `help:"Destination mailbox" short:"d" required:""`
Query string `help:"Move messages matching search query (e.g., 'subject:newsletter')"`
Mailbox string `help:"Source mailbox" short:"m" default:"INBOX"`
}

type MailArchiveCmd struct {
IDs []string `arg:"" optional:"" help:"Message sequence number(s) or uid:<uid> to archive"`
Query string `help:"Archive messages matching search query (e.g., 'subject:newsletter')"`
Mailbox string `help:"Source mailbox" short:"m" default:"INBOX"`
}

type MailFlagCmd struct {
IDs []string `arg:"" optional:"" help:"Message ID(s)"`
IDs []string `arg:"" optional:"" help:"Message sequence number(s) or uid:<uid>"`
Query string `help:"Flag messages matching search query (e.g., 'from:user@example.com')"`
Mailbox string `help:"Mailbox to operate on" short:"m" default:"INBOX"`
Read bool `help:"Mark as read" xor:"read"`
Expand Down
18 changes: 18 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func TestMailReadCmdOptions(t *testing.T) {
Raw: true,
Headers: true,
Attachments: true,
Unread: true,
}

if cmd.ID != "123" {
Expand All @@ -138,6 +139,9 @@ func TestMailReadCmdOptions(t *testing.T) {
if !cmd.Attachments {
t.Error("Attachments should be true")
}
if !cmd.Unread {
t.Error("Unread should be true")
}
}

func TestMailSendCmdOptions(t *testing.T) {
Expand Down Expand Up @@ -189,6 +193,20 @@ func TestMailMoveCmdOptions(t *testing.T) {
}
}

func TestMailArchiveCmdOptions(t *testing.T) {
cmd := MailArchiveCmd{
IDs: []string{"123"},
Mailbox: "INBOX",
}

if len(cmd.IDs) != 1 || cmd.IDs[0] != "123" {
t.Errorf("IDs = %v, want [123]", cmd.IDs)
}
if cmd.Mailbox != "INBOX" {
t.Errorf("Mailbox = %q, want %q", cmd.Mailbox, "INBOX")
}
}

func TestMailFlagCmdOptions(t *testing.T) {
cmd := MailFlagCmd{
IDs: []string{"123"},
Expand Down
Loading