Skip to content
Open
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
84 changes: 83 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ For more information on JMAP, see also [the JMAP Crash Course](https://jmap.io/c
* [fileNodes mkdir](#filenodes-mkdir)
* [fileNodes rm](#filenodes-rm)
* [fileNodes cp](#filenodes-cp)
* [Sieve](#sieve)
* [sieve get](#sieve-get)
* [sieve create](#sieve-create)
* [sieve delete](#sieve-delete)
* [sieve changes](#sieve-changes)
* [sieve activate](#sieve-activate)
* [sieve deactivate](#sieve-deactivate)
* [License](#license)
* [Acknowledgement](#acknowledgement)
<!-- TOC -->
Expand All @@ -78,9 +85,10 @@ Dart supports Linux/ Windows/ MacOS. The above command does not depend on your o

## Features

* We support the jmap functions `/get` `/set`, and `/changes` for Contacts, AddressBooks, CalendarEvents, Calendars, Emails, Mailboxes, and FileNodes.
* We support the jmap functions `/get` `/set`, and `/changes` for Contacts, AddressBooks, CalendarEvents, Calendars, Emails, Mailboxes, FileNodes, and Sieve scripts.
* For Emails we additionally support `/query` for filtering and paging, `/import` for storing raw MIME messages, and `EmailSubmission/set` for sending.
* In the FileNode case we support (mostly) rsync-inspired file management operations.
* For Sieve scripts we additionally support activating and deactivating a script.

### Supported Sub-Commands

Expand All @@ -96,6 +104,7 @@ The general command structure is
* `identity`
* `mailbox`
* `session`
* `sieve`

The available operations depend on the object type (and will be explained in their corresponding section), but common ones are
* `get` gets all items ot the object type
Expand Down Expand Up @@ -510,6 +519,79 @@ Usage example:
jmap_cli mailbox changes --url <server-url> -u <user> -p <pass> --state <state>
```

## Sieve

Manage Sieve scripts (mail filtering rules) on the JMAP server. The script content is treated as an opaque string; this CLI does not parse or validate Sieve syntax.

### sieve get
Fetch all sieve scripts or a single script by id.

Options:
- `--id`, `-i` : Id of the sieve script to fetch

Usage examples:
```bash
jmap_cli sieve get --url <server-url> -u <user> -p <pass>
jmap_cli sieve get --url <server-url> -u <user> -p <pass> -i <scriptId>
```

### sieve create
Create a sieve script from a file or inline content.

Options:
- `--name`, `-n` : Name of the sieve script (required)
- `--file`, `-f` : Path to a file containing the sieve script source
- `--content` : Sieve script source, given inline instead of a file

Usage examples:
```bash
jmap_cli sieve create --url <server-url> -u <user> -p <pass> --name myFilter --file myFilter.sieve
jmap_cli sieve create --url <server-url> -u <user> -p <pass> --name myFilter --content 'require ["fileinto"]; keep;'
```

### sieve delete
Delete a sieve script by id.

Options:
- `--id`, `-i` : Id of the sieve script to delete

Usage example:
```bash
jmap_cli sieve delete --url <server-url> -u <user> -p <pass> -i <scriptId>
```

### sieve changes
Fetch sieve script changes since a given state.

Options:
- `--state` : If omitted, the CLI fetches the latest state automatically

Usage example:
```bash
jmap_cli sieve changes --url <server-url> -u <user> -p <pass> --state <state>
```

Note: this is part of the JMAP Sieve specification, but we are not aware of a working server implementation of it yet (e.g. Stalwart v1.0.0 returns an `unknownMethod` error), so the CLI will fail gracefully with a message stating this.

### sieve activate
Activate a sieve script by id. Only one script can be active per account at a time; activating one replaces whichever script was previously active.

Options:
- `--id`, `-i` : Id of the sieve script to activate

Usage example:
```bash
jmap_cli sieve activate --url <server-url> -u <user> -p <pass> -i <scriptId>
```

### sieve deactivate
Deactivate whichever sieve script is currently active, if any.

Usage example:
```bash
jmap_cli sieve deactivate --url <server-url> -u <user> -p <pass>
```

## FileNodes

Manage files and folders on the JMAP server.
Expand Down
9 changes: 8 additions & 1 deletion lib/jmap_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ export 'src/commands/mailbox/mailbox_command.dart';
export 'src/commands/mailbox/get_mailbox_command.dart';
export 'src/commands/mailbox/create_mailbox_command.dart';
export 'src/commands/mailbox/delete_mailbox_command.dart';
export 'src/commands/mailbox/changes_mailbox_command.dart';
export 'src/commands/mailbox/changes_mailbox_command.dart';
export 'src/commands/sieve/sieve_command.dart';
export 'src/commands/sieve/get_sieve_command.dart';
export 'src/commands/sieve/create_sieve_command.dart';
export 'src/commands/sieve/delete_sieve_command.dart';
export 'src/commands/sieve/changes_sieve_command.dart';
export 'src/commands/sieve/activate_sieve_command.dart';
export 'src/commands/sieve/deactivate_sieve_command.dart';
44 changes: 44 additions & 0 deletions lib/src/commands/sieve/activate_sieve_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:jmap_cli/src/commands/base_command.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/util/sieve_util.dart';

class ActivateSieveCommand extends BaseCommand {
@override
final name = 'activate';

@override
List<String> get aliases => ['sieve.activate'];

@override
final description = 'Activate a sieve script by id';

@override
Future<int> run() async {
try {
final args = argResults;
if (args == null) return 1;

final idArg = args['id'];
if (idArg == null || idArg.toString().trim().isEmpty) {
print('Error: --id is required');
return 1;
}

final live = await buildClientAndAccount(args, 'POST');
final accountId = AccountId(Id(args['accountId'] ?? live.accountId.id.value));

await SieveUtil.activateSieveScript(
client: live.httpClient,
accountId: accountId,
id: idArg,
);

print('Sieve script activated!');
return 0;
} catch (e) {
print('Error: $e');
return 1;
}
}
}
79 changes: 79 additions & 0 deletions lib/src/commands/sieve/changes_sieve_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'dart:convert';
import 'package:jmap_cli/src/commands/base_command.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart';
import 'package:jmap_dart_client/jmap/core/error/method/exception/error_method_response_exception.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:jmap_dart_client/util/sieve_util.dart';

class ChangesSieveCommand extends BaseCommand {
@override
final name = 'changes';

@override
List<String> get aliases => ['sieve.changes'];

@override
final description = 'Get change information for sieve scripts as JSON';

ChangesSieveCommand() {
argParser.addOption(
'state',
help: 'The previous state to check changes from. If omitted, fetches current state first.',
);
}

@override
Future<int> run() async {
try {
final args = argResults;
if (args == null) return 1;

final live = await buildClientAndAccount(args, 'GET');
final accountId = AccountId(Id(args['accountId'] ?? live.accountId.id.value));

State fromState;
final stateArg = args['state'];

if (stateArg != null && stateArg.trim().isNotEmpty) {
fromState = State(stateArg.trim());
} else {
final getResp = await SieveUtil.getSieveScripts(
client: live.httpClient,
accountId: accountId,
);
fromState = getResp.state;
}

final changes = await SieveUtil.changesSieveScripts(
client: live.httpClient,
accountId: accountId,
sinceState: fromState,
);

print(jsonEncode({
'accountId': changes.accountId.id.value,
'oldState': changes.oldState.value,
'newState': changes.newState.value,
'hasMoreChanges': changes.hasMoreChanges,
'created': changes.created.map((id) => id.value).toList(),
'updated': changes.updated.map((id) => id.value).toList(),
'destroyed': changes.destroyed.map((id) => id.value).toList(),
}));
return 0;
} on ErrorMethodResponseException catch (e) {
final errorResponse = e.errorResponse;
if (errorResponse is ErrorMethodResponse &&
errorResponse.type == ErrorMethodResponse.unknownMethod) {
print('Error: this server does not support SieveScript/changes.');
return 1;
}
print('Error: $e');
return 1;
} catch (e) {
print('Error: $e');
return 1;
}
}
}
83 changes: 83 additions & 0 deletions lib/src/commands/sieve/create_sieve_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'dart:io';
import 'package:jmap_cli/src/commands/base_command.dart';
import 'package:jmap_cli/src/commands/session/get_session_command.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/util/sieve_util.dart';

class CreateSieveCommand extends BaseCommand {
@override
final name = 'create';

@override
List<String> get aliases => ['sieve.create'];

@override
final description = 'Create a sieve script from a file or inline content';

CreateSieveCommand() {
argParser
..addOption('name', abbr: 'n', help: 'Name of the sieve script.', mandatory: true)
..addOption('file', abbr: 'f', help: 'Path to a file containing the sieve script source.')
..addOption('content', help: 'Sieve script source, given inline instead of a file.');
}

@override
Future<int> run() async {
try {
final args = argResults;
if (args == null) return 1;

final name = args['name'] as String;
final filePath = args['file'] as String?;
final inlineContent = args['content'] as String?;

String content;
if (filePath != null) {
final file = File(filePath);
if (!file.existsSync()) {
print('File not found: $filePath');
return 1;
}
content = await file.readAsString();
} else if (inlineContent != null) {
content = inlineContent;
} else {
print('Error: either --file or --content is required');
return 1;
}

final live = await GetSessionCommand().getLiveClient(args, 'POST');
final accountId = AccountId(Id(args['accountId'] ?? live.accountId.id.value));

final uploadTemplate = Uri.decodeFull(live.session.uploadUrl.toString())
.replaceFirst('{accountId}', accountId.id.value);
// Cyrus returns a server-relative path; make it absolute to avoid Dio
// appending it to the JMAP API path and producing a duplicate segment.
final uploadUrl = uploadTemplate.startsWith('http://') || uploadTemplate.startsWith('https://')
? uploadTemplate
: Uri.parse(live.dio.options.baseUrl)
.replace(path: uploadTemplate)
.toString();

final id = await SieveUtil.createSieveScript(
client: live.httpClient,
accountId: accountId,
name: name,
content: content,
httpUploadUrl: uploadUrl,
);

if (id != null) {
print(id);
return 0;
}

print('Sieve script could not be created!');
return 1;
} catch (e) {
print('Error: $e');
return 1;
}
}
}
37 changes: 37 additions & 0 deletions lib/src/commands/sieve/deactivate_sieve_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:jmap_cli/src/commands/base_command.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/util/sieve_util.dart';

class DeactivateSieveCommand extends BaseCommand {
@override
final name = 'deactivate';

@override
List<String> get aliases => ['sieve.deactivate'];

@override
final description = 'Deactivate the currently active sieve script';

@override
Future<int> run() async {
try {
final args = argResults;
if (args == null) return 1;

final live = await buildClientAndAccount(args, 'POST');
final accountId = AccountId(Id(args['accountId'] ?? live.accountId.id.value));

await SieveUtil.deactivateActiveSieveScript(
client: live.httpClient,
accountId: accountId,
);

print('Sieve script deactivated!');
return 0;
} catch (e) {
print('Error: $e');
return 1;
}
}
}
Loading