diff --git a/.gitignore b/.gitignore index a247422..62d9ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Miscellaneous *.class +*.lock *.log *.pyc *.swp @@ -15,20 +16,46 @@ *.iws .idea/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/ + +# Flutter repo-specific +/bin/cache/ +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies +**/generated_plugin_registrant.dart .packages .pub-cache/ .pub/ build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds # Android related **/android/**/gradle-wrapper.jar @@ -38,6 +65,8 @@ build/ **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks # iOS/XCode related **/ios/**/*.mode1v3 @@ -56,11 +85,11 @@ build/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ +**/ios/Flutter/.last_build_id **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ @@ -68,8 +97,22 @@ build/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* +# macOS +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/Flutter-Debug.xcconfig +**/macos/Flutter/Flutter-Release.xcconfig +**/macos/Flutter/Flutter-Profile.xcconfig + +# Coverage +coverage/ + +# Symbols +app.*.symbols + # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock diff --git a/lib/http/converter/account_id_converter.dart b/lib/http/converter/account_id_converter.dart new file mode 100644 index 0000000..c03e3e2 --- /dev/null +++ b/lib/http/converter/account_id_converter.dart @@ -0,0 +1,13 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class AccountIdConverter implements JsonConverter { + const AccountIdConverter(); + + @override + AccountId fromJson(String json) => AccountId(Id(json)); + + @override + String toJson(AccountId object) => object.id.value; +} \ No newline at end of file diff --git a/lib/http/converter/capability_identifier_onverter.dart b/lib/http/converter/capability_identifier_onverter.dart new file mode 100644 index 0000000..c4a1fdc --- /dev/null +++ b/lib/http/converter/capability_identifier_onverter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/core/capability/capability.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class CapabilityIdentifierConverter implements JsonConverter { + const CapabilityIdentifierConverter(); + + @override + CapabilityIdentifier fromJson(String json) => CapabilityIdentifier(json); + + @override + String toJson(CapabilityIdentifier object) => object.value; +} \ No newline at end of file diff --git a/lib/http/converter/id_converter.dart b/lib/http/converter/id_converter.dart new file mode 100644 index 0000000..e51aad2 --- /dev/null +++ b/lib/http/converter/id_converter.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class IdConverter implements JsonConverter { + const IdConverter(); + + @override + Id fromJson(String json) { + return Id(json); + } + + @override + String toJson(Id object) { + return object.value; + } + +} \ No newline at end of file diff --git a/lib/http/converter/is_subscribed_converter.dart b/lib/http/converter/is_subscribed_converter.dart new file mode 100644 index 0000000..d74549b --- /dev/null +++ b/lib/http/converter/is_subscribed_converter.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class IsSubscribedConverter implements JsonConverter { + const IsSubscribedConverter(); + + @override + IsSubscribed? fromJson(bool? json) { + return json != null ? IsSubscribed(json) : null; + } + + @override + bool? toJson(IsSubscribed? object) { + return object?.value; + } +} \ No newline at end of file diff --git a/lib/http/converter/mailbox_id_converter.dart b/lib/http/converter/mailbox_id_converter.dart new file mode 100644 index 0000000..aeaa999 --- /dev/null +++ b/lib/http/converter/mailbox_id_converter.dart @@ -0,0 +1,13 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class MailboxIdConverter implements JsonConverter { + const MailboxIdConverter(); + + @override + MailboxId fromJson(String json) => MailboxId(Id(json)); + + @override + String toJson(MailboxId object) => object.id.value; +} \ No newline at end of file diff --git a/lib/http/converter/mailbox_id_nullable_converter.dart b/lib/http/converter/mailbox_id_nullable_converter.dart new file mode 100644 index 0000000..8f01f4e --- /dev/null +++ b/lib/http/converter/mailbox_id_nullable_converter.dart @@ -0,0 +1,13 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class MailboxIdNullableConverter implements JsonConverter { + const MailboxIdNullableConverter(); + + @override + MailboxId? fromJson(String? json) => json != null ? MailboxId(Id(json)) : null; + + @override + String? toJson(MailboxId? object) => object?.id.value; +} \ No newline at end of file diff --git a/lib/http/converter/mailbox_name_converter.dart b/lib/http/converter/mailbox_name_converter.dart new file mode 100644 index 0000000..41eb77a --- /dev/null +++ b/lib/http/converter/mailbox_name_converter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class MailboxNameConverter implements JsonConverter { + const MailboxNameConverter(); + + @override + MailboxName? fromJson(String? json) => json != null ? MailboxName(json) : null; + + @override + String? toJson(MailboxName? object) => object?.name; +} \ No newline at end of file diff --git a/lib/http/converter/method_call_id_converter.dart b/lib/http/converter/method_call_id_converter.dart new file mode 100644 index 0000000..1dc735a --- /dev/null +++ b/lib/http/converter/method_call_id_converter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class MethodCallIdConverter implements JsonConverter { + const MethodCallIdConverter(); + + @override + MethodCallId fromJson(String json) => MethodCallId(json); + + @override + String toJson(MethodCallId object) => object.value; +} \ No newline at end of file diff --git a/lib/http/converter/method_name_converter.dart b/lib/http/converter/method_name_converter.dart new file mode 100644 index 0000000..25fbfea --- /dev/null +++ b/lib/http/converter/method_name_converter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class MethodNameConverter implements JsonConverter { + const MethodNameConverter(); + + @override + MethodName fromJson(String json) => MethodName(json); + + @override + String toJson(MethodName object) => object.value; +} \ No newline at end of file diff --git a/lib/http/converter/method_response_converter.dart b/lib/http/converter/method_response_converter.dart new file mode 100644 index 0000000..61db267 --- /dev/null +++ b/lib/http/converter/method_response_converter.dart @@ -0,0 +1,17 @@ + +import 'package:jmap_dart_client/jmap/core/method/method_response.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class MethodResponseConverter implements JsonConverter { + const MethodResponseConverter(); + + @override + MethodResponse fromJson(dynamic json) { + return this.fromJson(json); + } + + @override + dynamic toJson(MethodResponse object) { + return object; + } +} \ No newline at end of file diff --git a/lib/http/converter/properties_converter.dart b/lib/http/converter/properties_converter.dart new file mode 100644 index 0000000..46da977 --- /dev/null +++ b/lib/http/converter/properties_converter.dart @@ -0,0 +1,25 @@ + +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class PropertiesConverter implements JsonConverter?> { + const PropertiesConverter(); + + @override + Properties? fromJson(List? json) { + return json != null ? Properties(json.toSet()) : null; + } + + @override + List? toJson(Properties? object) { + if (object == null) { + return null; + } + + if (object.value.isEmpty) { + return null; + } + + return object.value.toList(); + } +} \ No newline at end of file diff --git a/lib/http/converter/reference_path_converter.dart b/lib/http/converter/reference_path_converter.dart new file mode 100644 index 0000000..1721f2e --- /dev/null +++ b/lib/http/converter/reference_path_converter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class ReferencePathConverter implements JsonConverter { + const ReferencePathConverter(); + + @override + ReferencePath fromJson(String json) => ReferencePath(json); + + @override + String toJson(ReferencePath object) => object.value; +} \ No newline at end of file diff --git a/lib/http/converter/request_invocation_converter.dart b/lib/http/converter/request_invocation_converter.dart new file mode 100644 index 0000000..9b4b51e --- /dev/null +++ b/lib/http/converter/request_invocation_converter.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; + +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class RequestInvocationConverter implements JsonConverter> { + const RequestInvocationConverter(); + + @override + RequestInvocation fromJson(List json) { + return RequestInvocation(MethodName(json[0]), jsonDecode(json[1]), jsonDecode(json[2])); + } + + @override + List toJson(RequestInvocation object) { + List list = List.empty(growable: true); + list.add(object.methodName.value); + list.add(object.arguments.value.toJson()); + list.add(object.methodCallId.value); + return list; + } +} \ No newline at end of file diff --git a/lib/http/converter/response_invocation_converter.dart b/lib/http/converter/response_invocation_converter.dart new file mode 100644 index 0000000..4ef7863 --- /dev/null +++ b/lib/http/converter/response_invocation_converter.dart @@ -0,0 +1,25 @@ + +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/response/response_invocation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class ResponseInvocationConverter implements JsonConverter> { + const ResponseInvocationConverter(); + + @override + ResponseInvocation fromJson(List json) { + if (json.length == 3) { + return ResponseInvocation( + MethodName(json[0]), + ResponseArguments(json[1]), + MethodCallId(json[2])); + } else { + throw Exception("Wrong response invocation"); + } + } + + @override + List toJson(ResponseInvocation object) { + return List.of({object.methodName, object.arguments, object.methodCallId}); + } +} \ No newline at end of file diff --git a/lib/http/converter/role_converter.dart b/lib/http/converter/role_converter.dart new file mode 100644 index 0000000..265516f --- /dev/null +++ b/lib/http/converter/role_converter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class RoleConverter implements JsonConverter { + const RoleConverter(); + + @override + Role? fromJson(String? json) => json != null ? Role(json) : null; + + @override + String? toJson(Role? object) => object?.value; +} \ No newline at end of file diff --git a/lib/http/converter/sort_order_converter.dart b/lib/http/converter/sort_order_converter.dart new file mode 100644 index 0000000..7787935 --- /dev/null +++ b/lib/http/converter/sort_order_converter.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class SortOrderConverter implements JsonConverter { + const SortOrderConverter(); + + @override + SortOrder? fromJson(int? json) { + return json != null ? SortOrder(sortValue: json) : null; + } + + @override + int? toJson(SortOrder? object) { + return object?.value.value.toInt(); + } +} \ No newline at end of file diff --git a/lib/http/converter/state_converter.dart b/lib/http/converter/state_converter.dart new file mode 100644 index 0000000..c4fce7c --- /dev/null +++ b/lib/http/converter/state_converter.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class StateConverter implements JsonConverter { + const StateConverter(); + + @override + State fromJson(String json) => State(json); + + @override + String toJson(State object) => object.value; +} \ No newline at end of file diff --git a/lib/http/converter/total_email_converter.dart b/lib/http/converter/total_email_converter.dart new file mode 100644 index 0000000..bfc35b7 --- /dev/null +++ b/lib/http/converter/total_email_converter.dart @@ -0,0 +1,17 @@ +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class TotalEmailConverter implements JsonConverter { + const TotalEmailConverter(); + + @override + TotalEmails? fromJson(int? json) { + return json != null ? TotalEmails(UnsignedInt(json)) : null; + } + + @override + int? toJson(TotalEmails? object) { + return object?.value.value.toInt(); + } +} \ No newline at end of file diff --git a/lib/http/converter/total_threads_converter.dart b/lib/http/converter/total_threads_converter.dart new file mode 100644 index 0000000..92f9d5e --- /dev/null +++ b/lib/http/converter/total_threads_converter.dart @@ -0,0 +1,17 @@ +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class TotalThreadsConverter implements JsonConverter { + const TotalThreadsConverter(); + + @override + TotalThreads? fromJson(int? json) { + return json != null ? TotalThreads(UnsignedInt(json)) : null; + } + + @override + int? toJson(TotalThreads? object) { + return object?.value.value.toInt(); + } +} \ No newline at end of file diff --git a/lib/http/converter/unread_emails_converter.dart b/lib/http/converter/unread_emails_converter.dart new file mode 100644 index 0000000..389be96 --- /dev/null +++ b/lib/http/converter/unread_emails_converter.dart @@ -0,0 +1,17 @@ +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class UnreadEmailsConverter implements JsonConverter { + const UnreadEmailsConverter(); + + @override + UnreadEmails? fromJson(int? json) { + return json != null ? UnreadEmails(UnsignedInt(json)) : null; + } + + @override + int? toJson(UnreadEmails? object) { + return object?.value.value.toInt(); + } +} \ No newline at end of file diff --git a/lib/http/converter/unread_threads_converter.dart b/lib/http/converter/unread_threads_converter.dart new file mode 100644 index 0000000..57fdfab --- /dev/null +++ b/lib/http/converter/unread_threads_converter.dart @@ -0,0 +1,17 @@ +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class UnreadThreadsConverter implements JsonConverter { + const UnreadThreadsConverter(); + + @override + UnreadThreads? fromJson(int? json) { + return json != null ? UnreadThreads(UnsignedInt(json)) : null; + } + + @override + int? toJson(UnreadThreads? object) { + return object?.value.value.toInt(); + } +} \ No newline at end of file diff --git a/lib/http/converter/unsigned_int_nullable_converter.dart b/lib/http/converter/unsigned_int_nullable_converter.dart new file mode 100644 index 0000000..e5bc233 --- /dev/null +++ b/lib/http/converter/unsigned_int_nullable_converter.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class UnsignedIntNullableConverter implements JsonConverter { + const UnsignedIntNullableConverter(); + + @override + UnsignedInt? fromJson(int? json) { + return json != null ? UnsignedInt(json) : null; + } + + @override + int? toJson(UnsignedInt? object) { + return object?.value.toInt(); + } +} \ No newline at end of file diff --git a/lib/http/http_client.dart b/lib/http/http_client.dart new file mode 100644 index 0000000..a10e95d --- /dev/null +++ b/lib/http/http_client.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/util/options_extensions.dart'; + +class HttpClient { + static const jmapHeader = 'application/json;jmapVersion=rfc-8621'; + + final Dio _dio; + + HttpClient(this._dio); + + Future> post( + String path, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + } + ) async { + final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader}) + ?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ; + + return await _dio.post( + path, + data: data, + queryParameters: queryParameters, + options: newOptions, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress) + .then((value) => value.data) + .catchError((error) => throw error); + } +} \ No newline at end of file diff --git a/lib/jmap/account_id.dart b/lib/jmap/account_id.dart new file mode 100644 index 0000000..1161cbf --- /dev/null +++ b/lib/jmap/account_id.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/http/converter/id_converter.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'account_id.g.dart'; + +@IdConverter() +@JsonSerializable() +class AccountId with EquatableMixin { + final Id id; + + AccountId(this.id); + + factory AccountId.fromJson(Map json) => _$AccountIdFromJson(json); + + Map toJson() => _$AccountIdToJson(this); + + @override + List get props => [id]; +} \ No newline at end of file diff --git a/lib/jmap/account_id.g.dart b/lib/jmap/account_id.g.dart new file mode 100644 index 0000000..7084dc0 --- /dev/null +++ b/lib/jmap/account_id.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_id.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AccountId _$AccountIdFromJson(Map json) { + return AccountId( + const IdConverter().fromJson(json['id'] as String), + ); +} + +Map _$AccountIdToJson(AccountId instance) => { + 'id': const IdConverter().toJson(instance.id), + }; diff --git a/lib/jmap/core/capability/capability.dart b/lib/jmap/core/capability/capability.dart new file mode 100644 index 0000000..140ecdf --- /dev/null +++ b/lib/jmap/core/capability/capability.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +class CapabilityIdentifier with EquatableMixin { + static final jmapCore = CapabilityIdentifier('urn:ietf:params:jmap:core'); + static final jmapMail = CapabilityIdentifier('urn:ietf:params:jmap:mail'); + + final String value; + + CapabilityIdentifier(this.value); + + @override + List get props => [value]; +} + +class CapabilityProperties {} + +abstract class Capability { + final CapabilityIdentifier identifier; + final CapabilityProperties properties; + + Capability(this.identifier, this.properties); +} + + + diff --git a/lib/jmap/core/id.dart b/lib/jmap/core/id.dart new file mode 100644 index 0000000..a2720b7 --- /dev/null +++ b/lib/jmap/core/id.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; +import 'package:quiver/check.dart'; + +class Id with EquatableMixin { + final RegExp _idCharacterConstraint = RegExp(r'^[a-zA-Z0-9]+[a-zA-Z0-9-_]*$'); + final String value; + + Id(this.value) { + checkArgument(value.isNotEmpty, message: 'invalid length'); + checkArgument(value.length < 255, message: 'invalid length'); + checkArgument(_idCharacterConstraint.hasMatch(value)); + } + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/jmap/core/method/method.dart b/lib/jmap/core/method/method.dart new file mode 100644 index 0000000..9ae89c8 --- /dev/null +++ b/lib/jmap/core/method/method.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; + +abstract class Method with EquatableMixin { + MethodName get methodName; + + Set get requiredCapabilities; + + Map toJson(); +} + +abstract class MethodRequiringAccountId extends Method { + final AccountId accountId; + + MethodRequiringAccountId(this.accountId); +} diff --git a/lib/jmap/core/method/method_response.dart b/lib/jmap/core/method/method_response.dart new file mode 100644 index 0000000..b872786 --- /dev/null +++ b/lib/jmap/core/method/method_response.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; + +abstract class MethodResponse with EquatableMixin { +} + +abstract class ResponseRequiringAccountId extends MethodResponse { + final AccountId accountId; + + ResponseRequiringAccountId(this.accountId); +} \ No newline at end of file diff --git a/lib/jmap/core/method/request/changes_method.dart b/lib/jmap/core/method/request/changes_method.dart new file mode 100644 index 0000000..ec30aba --- /dev/null +++ b/lib/jmap/core/method/request/changes_method.dart @@ -0,0 +1,14 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/method/method.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:json_annotation/json_annotation.dart'; + +abstract class ChangesMethod extends MethodRequiringAccountId { + final State sinceState; + + @JsonKey(includeIfNull: false) + final UnsignedInt? maxChanges; + + ChangesMethod(AccountId accountId, this.sinceState, {this.maxChanges}) : super(accountId); +} \ No newline at end of file diff --git a/lib/jmap/core/method/request/get_method.dart b/lib/jmap/core/method/request/get_method.dart new file mode 100644 index 0000000..d17991f --- /dev/null +++ b/lib/jmap/core/method/request/get_method.dart @@ -0,0 +1,50 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/method/method.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/request/result_reference.dart'; +import 'package:json_annotation/json_annotation.dart'; + +abstract class GetMethod extends MethodRequiringAccountId + with OptionalIds, OptionalProperties, OptionalReferenceIds, OptionalReferenceProperties { + GetMethod(AccountId accountId) : super(accountId); +} + +mixin OptionalIds { + @JsonKey(includeIfNull: false) + Set? ids; + + void addIds(Set values) { + if (ids == null) { + ids = Set(); + } + ids?.addAll(values); + } +} + +mixin OptionalReferenceIds { + @JsonKey(name: '#ids', includeIfNull: false) + ResultReference? referenceIds; + + void addReferenceIds(ResultReference resultReferenceIds) { + referenceIds = resultReferenceIds; + } +} + +mixin OptionalProperties { + @JsonKey(includeIfNull: false) + Properties? properties = Properties.empty(); + + void addProperties(Properties other) { + properties = properties?.union(other); + } +} + +mixin OptionalReferenceProperties { + @JsonKey(name: '#properties', includeIfNull: false) + ResultReference? referenceProperties; + + void addReferenceProperties(ResultReference resultReferenceProperties) { + referenceProperties = resultReferenceProperties; + } +} \ No newline at end of file diff --git a/lib/jmap/core/method/response/changes_response.dart b/lib/jmap/core/method/response/changes_response.dart new file mode 100644 index 0000000..b85b0ac --- /dev/null +++ b/lib/jmap/core/method/response/changes_response.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/method/method_response.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; + +abstract class ChangesResponse extends ResponseRequiringAccountId { + final State oldState; + final State newState; + final bool hasMoreChanges; + final Set created; + final Set updated; + final Set destroyed; + + ChangesResponse(AccountId accountId, this.oldState, this.newState, this.hasMoreChanges, + this.created, this.updated, this.destroyed) : super(accountId); +} \ No newline at end of file diff --git a/lib/jmap/core/method/response/get_response.dart b/lib/jmap/core/method/response/get_response.dart new file mode 100644 index 0000000..70db854 --- /dev/null +++ b/lib/jmap/core/method/response/get_response.dart @@ -0,0 +1,12 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/method/method_response.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; + +abstract class GetResponse extends ResponseRequiringAccountId { + final State state; + final List list; + final List? notFound; + + GetResponse(AccountId accountId, this.state, this.list, this.notFound) : super(accountId); +} \ No newline at end of file diff --git a/lib/jmap/core/properties/properties.dart b/lib/jmap/core/properties/properties.dart new file mode 100644 index 0000000..1cd16b6 --- /dev/null +++ b/lib/jmap/core/properties/properties.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; + +class Properties with EquatableMixin { + final Set value; + + Properties(this.value); + + static Properties empty() => Properties(Set()); + + Properties union(Properties other) => Properties(value.union(other.value)); + + Properties removeAll(Properties other) => Properties(value..removeAll(other.value)); + + bool isEmpty() => value.isEmpty; + + Properties operator +(Properties other) => union(other); + + Properties operator -(Properties other) => removeAll(other); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/jmap/core/request/ready_to_build.dart b/lib/jmap/core/request/ready_to_build.dart new file mode 100644 index 0000000..967c59b --- /dev/null +++ b/lib/jmap/core/request/ready_to_build.dart @@ -0,0 +1,5 @@ +import 'package:jmap_dart_client/jmap/core/request/request_object.dart'; + +mixin ReadyToBuild { + RequestObject build(); +} \ No newline at end of file diff --git a/lib/jmap/core/request/reference_path.dart b/lib/jmap/core/request/reference_path.dart new file mode 100644 index 0000000..f05c966 --- /dev/null +++ b/lib/jmap/core/request/reference_path.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class ReferencePath with EquatableMixin { + static ReferencePath createdPath = ReferencePath('created/*'); + static ReferencePath updatedPath = ReferencePath('updated/*'); + static ReferencePath updatedPropertiesPath = ReferencePath('updatedProperties'); + + final String value; + + ReferencePath(this.value); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/jmap/core/request/request_invocation.dart b/lib/jmap/core/request/request_invocation.dart new file mode 100644 index 0000000..9572948 --- /dev/null +++ b/lib/jmap/core/request/request_invocation.dart @@ -0,0 +1,41 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/request/result_reference.dart'; + +import '../method/method.dart'; + +class RequestInvocation { + final MethodName methodName; + final Arguments arguments; + final MethodCallId methodCallId; + + RequestInvocation(this.methodName, this.arguments, this.methodCallId); + + ResultReference createResultReference(ReferencePath path) { + return ResultReference(methodCallId, arguments.value.methodName, path); + } +} + +class MethodName with EquatableMixin { + final String value; + + MethodName(this.value); + + @override + List get props => [value]; +} + +class Arguments { + final T value; + + Arguments(this.value); +} + +class MethodCallId with EquatableMixin { + final String value; + + MethodCallId(this.value); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/jmap/core/request/request_object.dart b/lib/jmap/core/request/request_object.dart new file mode 100644 index 0000000..161ee7e --- /dev/null +++ b/lib/jmap/core/request/request_object.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/http/converter/capability_identifier_onverter.dart'; +import 'package:jmap_dart_client/http/converter/request_invocation_converter.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/request/require_method_call.dart'; +import 'package:jmap_dart_client/jmap/core/request/require_using.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'request_object.g.dart'; + +@CapabilityIdentifierConverter() +@RequestInvocationConverter() +@JsonSerializable() +class RequestObject with EquatableMixin { + final Set using; + final List methodCalls; + + RequestObject(this.using, this.methodCalls); + + @override + List get props => [using, methodCalls]; + + Map toJson() => _$RequestObjectToJson(this); + + static RequestObjectBuilder builder() { + return RequestObjectBuilder(); + } +} + +class RequestObjectBuilder with RequiredUsing, RequireMethodCall { + + RequestObject build() { + return RequestObject( + capabilitiesBuilder.build().asSet(), + invocationsBuilder.build().asList()); + } +} diff --git a/lib/jmap/core/request/request_object.g.dart b/lib/jmap/core/request/request_object.g.dart new file mode 100644 index 0000000..ea14e7f --- /dev/null +++ b/lib/jmap/core/request/request_object.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'request_object.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RequestObject _$RequestObjectFromJson(Map json) { + return RequestObject( + (json['using'] as List) + .map((e) => const CapabilityIdentifierConverter().fromJson(e as String)) + .toSet(), + (json['methodCalls'] as List) + .map((e) => const RequestInvocationConverter().fromJson(e as List)) + .toList(), + ); +} + +Map _$RequestObjectToJson(RequestObject instance) => + { + 'using': instance.using + .map(const CapabilityIdentifierConverter().toJson) + .toList(), + 'methodCalls': instance.methodCalls + .map(const RequestInvocationConverter().toJson) + .toList(), + }; diff --git a/lib/jmap/core/request/require_method_call.dart b/lib/jmap/core/request/require_method_call.dart new file mode 100644 index 0000000..6d4674f --- /dev/null +++ b/lib/jmap/core/request/require_method_call.dart @@ -0,0 +1,11 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:jmap_dart_client/jmap/core/method/method.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; + +mixin RequireMethodCall { + final ListBuilder invocationsBuilder = ListBuilder(); + + void methodCalls(List newInvocations) { + invocationsBuilder.addAll(newInvocations); + } +} \ No newline at end of file diff --git a/lib/jmap/core/request/require_using.dart b/lib/jmap/core/request/require_using.dart new file mode 100644 index 0000000..30487ec --- /dev/null +++ b/lib/jmap/core/request/require_using.dart @@ -0,0 +1,16 @@ + +import 'package:built_collection/built_collection.dart'; + +import '../capability/capability.dart'; + +mixin RequiredUsing { + final SetBuilder capabilitiesBuilder = SetBuilder(); + + void using(CapabilityIdentifier capabilityIdentifier) { + capabilitiesBuilder.add(capabilityIdentifier); + } + + void usings(Set capabilityIdentifiers) { + capabilitiesBuilder.addAll(capabilityIdentifiers); + } +} \ No newline at end of file diff --git a/lib/jmap/core/request/result_reference.dart b/lib/jmap/core/request/result_reference.dart new file mode 100644 index 0000000..71d6c3f --- /dev/null +++ b/lib/jmap/core/request/result_reference.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/http/converter/method_call_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/method_name_converter.dart'; +import 'package:jmap_dart_client/http/converter/reference_path_converter.dart'; +import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'result_reference.g.dart'; + +@ReferencePathConverter() +@MethodNameConverter() +@MethodCallIdConverter() +@JsonSerializable() +class ResultReference with EquatableMixin { + final MethodCallId resultOf; + final MethodName name; + final ReferencePath path; + + ResultReference(this.resultOf, this.name, this.path); + + @override + List get props => [resultOf, name, path]; + + factory ResultReference.fromJson(Map json) => _$ResultReferenceFromJson(json); + + Map toJson() => _$ResultReferenceToJson(this); +} \ No newline at end of file diff --git a/lib/jmap/core/request/result_reference.g.dart b/lib/jmap/core/request/result_reference.g.dart new file mode 100644 index 0000000..483f540 --- /dev/null +++ b/lib/jmap/core/request/result_reference.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'result_reference.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ResultReference _$ResultReferenceFromJson(Map json) { + return ResultReference( + const MethodCallIdConverter().fromJson(json['resultOf'] as String), + const MethodNameConverter().fromJson(json['name'] as String), + const ReferencePathConverter().fromJson(json['path'] as String), + ); +} + +Map _$ResultReferenceToJson(ResultReference instance) => + { + 'resultOf': const MethodCallIdConverter().toJson(instance.resultOf), + 'name': const MethodNameConverter().toJson(instance.name), + 'path': const ReferencePathConverter().toJson(instance.path), + }; diff --git a/lib/jmap/core/response/response_invocation.dart b/lib/jmap/core/response/response_invocation.dart new file mode 100644 index 0000000..93b8831 --- /dev/null +++ b/lib/jmap/core/response/response_invocation.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; + + +class ResponseInvocation { + final MethodName methodName; + final ResponseArguments arguments; + final MethodCallId methodCallId; + + ResponseInvocation(this.methodName, this.arguments, this.methodCallId); +} + +class ResponseArguments { + final dynamic value; + + ResponseArguments(this.value); +} \ No newline at end of file diff --git a/lib/jmap/core/response/response_object.dart b/lib/jmap/core/response/response_object.dart new file mode 100644 index 0000000..6c67b27 --- /dev/null +++ b/lib/jmap/core/response/response_object.dart @@ -0,0 +1,39 @@ +import 'dart:developer' as developer; + +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/http/converter/response_invocation_converter.dart'; +import 'package:jmap_dart_client/http/converter/state_converter.dart'; +import 'package:jmap_dart_client/jmap/core/method/method_response.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/response/response_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'response_object.g.dart'; + +@StateConverter() +@ResponseInvocationConverter() +@JsonSerializable() +class ResponseObject with EquatableMixin { + final List methodResponses; + final State sessionState; + + ResponseObject(this.methodResponses, this.sessionState); + + factory ResponseObject.fromJson(Map json) => _$ResponseObjectFromJson(json); + + Map toJson() => _$ResponseObjectToJson(this); + + T? parse(MethodCallId methodCallId, T fromJson(Map o)) { + try { + final matchedResponse = methodResponses.firstWhere((method) => method.methodCallId == methodCallId); + return fromJson(matchedResponse.arguments.value); + } catch(error) { + developer.log('$error'); + return null; + } + } + + @override + List get props => [methodResponses, sessionState]; +} \ No newline at end of file diff --git a/lib/jmap/core/response/response_object.g.dart b/lib/jmap/core/response/response_object.g.dart new file mode 100644 index 0000000..d002c65 --- /dev/null +++ b/lib/jmap/core/response/response_object.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'response_object.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ResponseObject _$ResponseObjectFromJson(Map json) { + return ResponseObject( + (json['methodResponses'] as List) + .map((e) => const ResponseInvocationConverter().fromJson(e as List)) + .toList(), + const StateConverter().fromJson(json['sessionState'] as String), + ); +} + +Map _$ResponseObjectToJson(ResponseObject instance) => + { + 'methodResponses': instance.methodResponses + .map(const ResponseInvocationConverter().toJson) + .toList(), + 'sessionState': const StateConverter().toJson(instance.sessionState), + }; diff --git a/lib/jmap/core/state.dart b/lib/jmap/core/state.dart new file mode 100644 index 0000000..fb26b72 --- /dev/null +++ b/lib/jmap/core/state.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; + +class State with EquatableMixin { + final String value; + + State(this.value); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/jmap/core/unsigned_int.dart b/lib/jmap/core/unsigned_int.dart new file mode 100644 index 0000000..1d8c382 --- /dev/null +++ b/lib/jmap/core/unsigned_int.dart @@ -0,0 +1,13 @@ +import 'package:quiver/check.dart'; + +class UnsignedInt { + static final defaultValue = UnsignedInt(0); + + final num value; + + // UnsignedInt in range [0...2^53-1]. + UnsignedInt(this.value) { + checkArgument(value >= 0); + checkArgument(value < 9007199254740992); + } +} \ No newline at end of file diff --git a/lib/jmap/jmap_request.dart b/lib/jmap/jmap_request.dart new file mode 100644 index 0000000..f397252 --- /dev/null +++ b/lib/jmap/jmap_request.dart @@ -0,0 +1,92 @@ + +import 'package:built_collection/built_collection.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/request/result_reference.dart'; +import 'package:jmap_dart_client/jmap/core/response/response_object.dart'; +import 'package:jmap_dart_client/util/util.dart'; +import 'package:quiver/check.dart'; + +import 'core/capability/capability.dart'; +import 'core/method/method.dart'; +import 'core/request/request_invocation.dart'; +import 'core/request/request_object.dart'; + +class JmapRequest { + final HttpClient _httpClient; + final BuiltSet _capabilities; + final BuiltMap _invocations; + + JmapRequest(this._httpClient, this._capabilities, this._invocations); + + RequestObject? _requestObject; + RequestObject? get requestObject => _requestObject; + + Future execute() async { + _requestObject = (RequestObject.builder() + ..usings(_capabilities.asSet()) + ..methodCalls(_invocations.values.toList())) + .build(); + + return _httpClient.post('/jmap', data: _requestObject?.toJson()) + .then((value) => extractData(value)) + .catchError((error) => throw error); + } + + ResponseObject extractData(Map body) { + return ResponseObject.fromJson(body); + } +} + +class JmapRequestBuilder { + final HttpClient _httpClient; + final ProcessingInvocation _processingInvocation; + final SetBuilder _capabilitiesBuilder = SetBuilder(); + + JmapRequestBuilder(this._httpClient, this._processingInvocation); + + RequestInvocation invocation(Method method, {MethodCallId? methodCallId}) { + final callId = methodCallId ?? _processingInvocation.generateMethodCallId(); + final RequestInvocation invocation = RequestInvocation( + method.methodName, + Arguments(method), + callId + ); + _processingInvocation.addMethod(callId, invocation); + return invocation; + } + + void usings(Set capabilityIdentifiers) { + _capabilitiesBuilder.addAll(capabilityIdentifiers); + } + + JmapRequest build() { + return JmapRequest(_httpClient, _capabilitiesBuilder.build(), _processingInvocation._invocations); + } +} + +class ProcessingInvocation { + static const String methodCallIdPrefix = 'c'; + late BuiltMap _invocations; + + ProcessingInvocation() { + _invocations = BuiltMap(); + } + + MethodCallId generateMethodCallId() { + return positiveIntegers + .map((item) => MethodCallId(methodCallIdPrefix + item.toString())) + .firstWhere((callId) => !_invocations.keys.contains(callId)); + } + + void addMethod(MethodCallId callId, RequestInvocation requestInvocation) { + _invocations = (_invocations.toBuilder() + ..addAll({callId: requestInvocation})) + .build(); + } + + ResultReference createResultReference(MethodCallId methodCallId, ReferencePath path) { + checkArgument(_invocations.containsKey(methodCallId), message: 'no matched method call id'); + return _invocations[methodCallId]!.createResultReference(path); + } +} diff --git a/lib/jmap/mail/mailbox/changes/changes_mailbox_method.dart b/lib/jmap/mail/mailbox/changes/changes_mailbox_method.dart new file mode 100644 index 0000000..b7bb372 --- /dev/null +++ b/lib/jmap/mail/mailbox/changes/changes_mailbox_method.dart @@ -0,0 +1,35 @@ +import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/state_converter.dart'; +import 'package:jmap_dart_client/http/converter/unsigned_int_nullable_converter.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability.dart'; +import 'package:jmap_dart_client/jmap/core/method/request/changes_method.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'changes_mailbox_method.g.dart'; + +@UnsignedIntNullableConverter() +@StateConverter() +@AccountIdConverter() +@JsonSerializable() +class ChangesMailboxMethod extends ChangesMethod { + ChangesMailboxMethod(AccountId accountId, State sinceState, {UnsignedInt? maxChanges}) + : super(accountId, sinceState, maxChanges: maxChanges); + + @override + MethodName get methodName => MethodName('Mailbox/changes'); + + @override + List get props => [accountId, sinceState, maxChanges]; + + @override + Set get requiredCapabilities => {CapabilityIdentifier.jmapMail}; + + factory ChangesMailboxMethod.fromJson(Map json) => _$ChangesMailboxMethodFromJson(json); + + @override + Map toJson() => _$ChangesMailboxMethodToJson(this); +} \ No newline at end of file diff --git a/lib/jmap/mail/mailbox/changes/changes_mailbox_method.g.dart b/lib/jmap/mail/mailbox/changes/changes_mailbox_method.g.dart new file mode 100644 index 0000000..74ebcac --- /dev/null +++ b/lib/jmap/mail/mailbox/changes/changes_mailbox_method.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'changes_mailbox_method.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChangesMailboxMethod _$ChangesMailboxMethodFromJson(Map json) { + return ChangesMailboxMethod( + const AccountIdConverter().fromJson(json['accountId'] as String), + const StateConverter().fromJson(json['sinceState'] as String), + maxChanges: const UnsignedIntNullableConverter() + .fromJson(json['maxChanges'] as int?), + ); +} + +Map _$ChangesMailboxMethodToJson( + ChangesMailboxMethod instance) { + final val = { + 'accountId': const AccountIdConverter().toJson(instance.accountId), + 'sinceState': const StateConverter().toJson(instance.sinceState), + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('maxChanges', + const UnsignedIntNullableConverter().toJson(instance.maxChanges)); + return val; +} diff --git a/lib/jmap/mail/mailbox/changes/changes_mailbox_response.dart b/lib/jmap/mail/mailbox/changes/changes_mailbox_response.dart new file mode 100644 index 0000000..6947fd0 --- /dev/null +++ b/lib/jmap/mail/mailbox/changes/changes_mailbox_response.dart @@ -0,0 +1,41 @@ +import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/id_converter.dart'; +import 'package:jmap_dart_client/http/converter/properties_converter.dart'; +import 'package:jmap_dart_client/http/converter/state_converter.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/method/response/changes_response.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'changes_mailbox_response.g.dart'; + +@PropertiesConverter() +@IdConverter() +@StateConverter() +@AccountIdConverter() +@JsonSerializable() +class ChangesMailboxResponse extends ChangesResponse { + final Properties? updatedProperties; + + ChangesMailboxResponse( + AccountId accountId, + State oldState, + State newState, + bool hasMoreChanges, + Set created, + Set updated, + Set destroyed, + {this.updatedProperties} + ) : super(accountId, oldState, newState, hasMoreChanges, created, updated, destroyed); + + factory ChangesMailboxResponse.fromJson(Map json) => _$ChangesMailboxResponseFromJson(json); + + static ChangesMailboxResponse deserialize(Map json) { + return ChangesMailboxResponse.fromJson(json); + } + + @override + List get props => [accountId, oldState, newState, hasMoreChanges, created, updated, destroyed, updatedProperties]; +} \ No newline at end of file diff --git a/lib/jmap/mail/mailbox/changes/changes_mailbox_response.g.dart b/lib/jmap/mail/mailbox/changes/changes_mailbox_response.g.dart new file mode 100644 index 0000000..2e2a3df --- /dev/null +++ b/lib/jmap/mail/mailbox/changes/changes_mailbox_response.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'changes_mailbox_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChangesMailboxResponse _$ChangesMailboxResponseFromJson( + Map json) { + return ChangesMailboxResponse( + const AccountIdConverter().fromJson(json['accountId'] as String), + const StateConverter().fromJson(json['oldState'] as String), + const StateConverter().fromJson(json['newState'] as String), + json['hasMoreChanges'] as bool, + (json['created'] as List) + .map((e) => const IdConverter().fromJson(e as String)) + .toSet(), + (json['updated'] as List) + .map((e) => const IdConverter().fromJson(e as String)) + .toSet(), + (json['destroyed'] as List) + .map((e) => const IdConverter().fromJson(e as String)) + .toSet(), + updatedProperties: const PropertiesConverter() + .fromJson(json['updatedProperties'] as List?), + ); +} + +Map _$ChangesMailboxResponseToJson( + ChangesMailboxResponse instance) => + { + 'accountId': const AccountIdConverter().toJson(instance.accountId), + 'oldState': const StateConverter().toJson(instance.oldState), + 'newState': const StateConverter().toJson(instance.newState), + 'hasMoreChanges': instance.hasMoreChanges, + 'created': instance.created.map(const IdConverter().toJson).toList(), + 'updated': instance.updated.map(const IdConverter().toJson).toList(), + 'destroyed': instance.destroyed.map(const IdConverter().toJson).toList(), + 'updatedProperties': + const PropertiesConverter().toJson(instance.updatedProperties), + }; diff --git a/lib/jmap/mail/mailbox/get/get_mailbox_method.dart b/lib/jmap/mail/mailbox/get/get_mailbox_method.dart new file mode 100644 index 0000000..b686140 --- /dev/null +++ b/lib/jmap/mail/mailbox/get/get_mailbox_method.dart @@ -0,0 +1,35 @@ +import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/id_converter.dart'; +import 'package:jmap_dart_client/http/converter/properties_converter.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability.dart'; +import 'package:jmap_dart_client/jmap/core/method/request/get_method.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/request/result_reference.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'get_mailbox_method.g.dart'; + +@IdConverter() +@AccountIdConverter() +@PropertiesConverter() +@JsonSerializable() +class GetMailboxMethod extends GetMethod { + GetMailboxMethod(AccountId accountId) : super(accountId); + + @override + MethodName get methodName => MethodName('Mailbox/get'); + + @override + Set get requiredCapabilities => { + CapabilityIdentifier.jmapCore, + CapabilityIdentifier.jmapMail + }; + + @override + List get props => [methodName, accountId, ids, properties, requiredCapabilities]; + + factory GetMailboxMethod.fromJson(Map json) => _$GetMailboxMethodFromJson(json); + + Map toJson() => _$GetMailboxMethodToJson(this); +} \ No newline at end of file diff --git a/lib/jmap/mail/mailbox/get/get_mailbox_method.g.dart b/lib/jmap/mail/mailbox/get/get_mailbox_method.g.dart new file mode 100644 index 0000000..67c577d --- /dev/null +++ b/lib/jmap/mail/mailbox/get/get_mailbox_method.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_mailbox_method.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetMailboxMethod _$GetMailboxMethodFromJson(Map json) { + return GetMailboxMethod( + const AccountIdConverter().fromJson(json['accountId'] as String), + ) + ..ids = (json['ids'] as List?) + ?.map((e) => const IdConverter().fromJson(e as String)) + .toSet() + ..referenceIds = json['#ids'] == null + ? null + : ResultReference.fromJson(json['#ids'] as Map) + ..properties = const PropertiesConverter() + .fromJson(json['properties'] as List?) + ..referenceProperties = json['#properties'] == null + ? null + : ResultReference.fromJson(json['#properties'] as Map); +} + +Map _$GetMailboxMethodToJson(GetMailboxMethod instance) { + final val = { + 'accountId': const AccountIdConverter().toJson(instance.accountId), + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('ids', instance.ids?.map(const IdConverter().toJson).toList()); + writeNotNull('#ids', instance.referenceIds); + writeNotNull( + 'properties', const PropertiesConverter().toJson(instance.properties)); + writeNotNull('#properties', instance.referenceProperties); + return val; +} diff --git a/lib/jmap/mail/mailbox/get/get_mailbox_response.dart b/lib/jmap/mail/mailbox/get/get_mailbox_response.dart new file mode 100644 index 0000000..d917404 --- /dev/null +++ b/lib/jmap/mail/mailbox/get/get_mailbox_response.dart @@ -0,0 +1,30 @@ +import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/id_converter.dart'; +import 'package:jmap_dart_client/http/converter/state_converter.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/method/response/get_response.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'get_mailbox_response.g.dart'; + +@StateConverter() +@AccountIdConverter() +@IdConverter() +@JsonSerializable() +class GetMailboxResponse extends GetResponse { + GetMailboxResponse(AccountId accountId, State state, List list, List? notFound) : super(accountId, state, list, notFound); + + factory GetMailboxResponse.fromJson(Map json) => _$GetMailboxResponseFromJson(json); + + static GetMailboxResponse deserialize(Map json) { + return GetMailboxResponse.fromJson(json); + } + + Map toJson() => _$GetMailboxResponseToJson(this); + + @override + List get props => [accountId, state, list, notFound]; +} \ No newline at end of file diff --git a/lib/jmap/mail/mailbox/get/get_mailbox_response.g.dart b/lib/jmap/mail/mailbox/get/get_mailbox_response.g.dart new file mode 100644 index 0000000..ffd1316 --- /dev/null +++ b/lib/jmap/mail/mailbox/get/get_mailbox_response.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_mailbox_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetMailboxResponse _$GetMailboxResponseFromJson(Map json) { + return GetMailboxResponse( + const AccountIdConverter().fromJson(json['accountId'] as String), + const StateConverter().fromJson(json['state'] as String), + (json['list'] as List) + .map((e) => Mailbox.fromJson(e as Map)) + .toList(), + (json['notFound'] as List?) + ?.map((e) => const IdConverter().fromJson(e as String)) + .toList(), + ); +} + +Map _$GetMailboxResponseToJson(GetMailboxResponse instance) => + { + 'accountId': const AccountIdConverter().toJson(instance.accountId), + 'state': const StateConverter().toJson(instance.state), + 'list': instance.list, + 'notFound': instance.notFound?.map(const IdConverter().toJson).toList(), + }; diff --git a/lib/jmap/mail/mailbox/mailbox.dart b/lib/jmap/mail/mailbox/mailbox.dart new file mode 100644 index 0000000..1a87e7b --- /dev/null +++ b/lib/jmap/mail/mailbox/mailbox.dart @@ -0,0 +1,152 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/http/converter/is_subscribed_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; +import 'package:jmap_dart_client/http/converter/role_converter.dart'; +import 'package:jmap_dart_client/http/converter/sort_order_converter.dart'; +import 'package:jmap_dart_client/http/converter/total_email_converter.dart'; +import 'package:jmap_dart_client/http/converter/total_threads_converter.dart'; +import 'package:jmap_dart_client/http/converter/unread_emails_converter.dart'; +import 'package:jmap_dart_client/http/converter/unread_threads_converter.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_rights.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'mailbox.g.dart'; + +@IsSubscribedConverter() +@UnreadThreadsConverter() +@UnreadEmailsConverter() +@TotalThreadsConverter() +@TotalEmailConverter() +@SortOrderConverter() +@RoleConverter() +@MailboxIdNullableConverter() +@MailboxNameConverter() +@MailboxIdConverter() +@JsonSerializable() +class Mailbox with EquatableMixin { + static Properties allProperties = Properties({ + 'id', 'name', 'parentId', 'role', 'sortOrder', 'totalEmails', 'unreadEmails', + 'totalThreads', 'unreadThreads', 'myRights', 'isSubscribed' + }); + + final MailboxId id; + final MailboxName? name; + final MailboxId? parentId; + final Role? role; + final SortOrder? sortOrder; + final TotalEmails? totalEmails; + final UnreadEmails? unreadEmails; + final TotalThreads? totalThreads; + final UnreadThreads? unreadThreads; + final MailboxRights? myRights; + final IsSubscribed? isSubscribed; + + Mailbox( + this.id, + this.name, + this.parentId, + this.role, + this.sortOrder, + this.totalEmails, + this.unreadEmails, + this.totalThreads, + this.unreadThreads, + this.myRights, + this.isSubscribed + ); + + factory Mailbox.fromJson(Map json) => _$MailboxFromJson(json); + + Map toJson() => _$MailboxToJson(this); + + @override + List get props => [id, name, parentId, role]; +} + +class MailboxId with EquatableMixin { + final Id id; + + MailboxId(this.id); + + @override + List get props => [id]; +} + +class MailboxName with EquatableMixin { + final String name; + + MailboxName(this.name); + + @override + List get props => [name]; +} + +class Role with EquatableMixin { + final String value; + + Role(this.value); + + @override + List get props => [value]; +} + +class SortOrder with EquatableMixin { + late final UnsignedInt value; + + SortOrder({int sortValue = 0}) { + this.value = UnsignedInt(sortValue); + } + + @override + List get props => [value]; +} + +class TotalEmails with EquatableMixin { + final UnsignedInt value; + + TotalEmails(this.value); + + @override + List get props => [value]; +} + +class UnreadEmails with EquatableMixin { + final UnsignedInt value; + + UnreadEmails(this.value); + + @override + List get props => [value]; +} + +class TotalThreads with EquatableMixin { + final UnsignedInt value; + + TotalThreads(this.value); + + @override + List get props => [value]; +} + +class UnreadThreads with EquatableMixin { + final UnsignedInt value; + + UnreadThreads(this.value); + + @override + List get props => [value]; +} + +class IsSubscribed with EquatableMixin { + final bool value; + + IsSubscribed(this.value); + + @override + List get props => [value]; +} diff --git a/lib/jmap/mail/mailbox/mailbox.g.dart b/lib/jmap/mail/mailbox/mailbox.g.dart new file mode 100644 index 0000000..0ab8d54 --- /dev/null +++ b/lib/jmap/mail/mailbox/mailbox.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mailbox.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Mailbox _$MailboxFromJson(Map json) { + return Mailbox( + const MailboxIdConverter().fromJson(json['id'] as String), + const MailboxNameConverter().fromJson(json['name'] as String?), + const MailboxIdNullableConverter().fromJson(json['parentId'] as String?), + const RoleConverter().fromJson(json['role'] as String?), + const SortOrderConverter().fromJson(json['sortOrder'] as int?), + const TotalEmailConverter().fromJson(json['totalEmails'] as int?), + const UnreadEmailsConverter().fromJson(json['unreadEmails'] as int?), + const TotalThreadsConverter().fromJson(json['totalThreads'] as int?), + const UnreadThreadsConverter().fromJson(json['unreadThreads'] as int?), + json['myRights'] == null + ? null + : MailboxRights.fromJson(json['myRights'] as Map), + const IsSubscribedConverter().fromJson(json['isSubscribed'] as bool?), + ); +} + +Map _$MailboxToJson(Mailbox instance) => { + 'id': const MailboxIdConverter().toJson(instance.id), + 'name': const MailboxNameConverter().toJson(instance.name), + 'parentId': const MailboxIdNullableConverter().toJson(instance.parentId), + 'role': const RoleConverter().toJson(instance.role), + 'sortOrder': const SortOrderConverter().toJson(instance.sortOrder), + 'totalEmails': const TotalEmailConverter().toJson(instance.totalEmails), + 'unreadEmails': + const UnreadEmailsConverter().toJson(instance.unreadEmails), + 'totalThreads': + const TotalThreadsConverter().toJson(instance.totalThreads), + 'unreadThreads': + const UnreadThreadsConverter().toJson(instance.unreadThreads), + 'myRights': instance.myRights, + 'isSubscribed': + const IsSubscribedConverter().toJson(instance.isSubscribed), + }; diff --git a/lib/jmap/mail/mailbox/mailbox_rights.dart b/lib/jmap/mail/mailbox/mailbox_rights.dart new file mode 100644 index 0000000..7352583 --- /dev/null +++ b/lib/jmap/mail/mailbox/mailbox_rights.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'mailbox_rights.g.dart'; + +@JsonSerializable() +class MailboxRights with EquatableMixin { + final bool mayReadItems; + final bool mayAddItems; + final bool mayRemoveItems; + final bool maySetSeen; + final bool maySetKeywords; + final bool mayCreateChild; + final bool mayRename; + final bool mayDelete; + final bool maySubmit; + + MailboxRights( + this.mayReadItems, + this.mayAddItems, + this.mayRemoveItems, + this.maySetSeen, + this.maySetKeywords, + this.mayCreateChild, + this.mayRename, + this.mayDelete, + this.maySubmit); + + factory MailboxRights.fromJson(Map json) { + return _$MailboxRightsFromJson(json); + } + + @override + List get props => [mayReadItems, mayAddItems, mayRemoveItems, maySetSeen, + maySetKeywords, mayCreateChild, mayRename, mayDelete, maySubmit]; +} diff --git a/lib/jmap/mail/mailbox/mailbox_rights.g.dart b/lib/jmap/mail/mailbox/mailbox_rights.g.dart new file mode 100644 index 0000000..2c83ded --- /dev/null +++ b/lib/jmap/mail/mailbox/mailbox_rights.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mailbox_rights.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MailboxRights _$MailboxRightsFromJson(Map json) { + return MailboxRights( + json['mayReadItems'] as bool, + json['mayAddItems'] as bool, + json['mayRemoveItems'] as bool, + json['maySetSeen'] as bool, + json['maySetKeywords'] as bool, + json['mayCreateChild'] as bool, + json['mayRename'] as bool, + json['mayDelete'] as bool, + json['maySubmit'] as bool, + ); +} + +Map _$MailboxRightsToJson(MailboxRights instance) => + { + 'mayReadItems': instance.mayReadItems, + 'mayAddItems': instance.mayAddItems, + 'mayRemoveItems': instance.mayRemoveItems, + 'maySetSeen': instance.maySetSeen, + 'maySetKeywords': instance.maySetKeywords, + 'mayCreateChild': instance.mayCreateChild, + 'mayRename': instance.mayRename, + 'mayDelete': instance.mayDelete, + 'maySubmit': instance.maySubmit, + }; diff --git a/lib/util/options_extensions.dart b/lib/util/options_extensions.dart new file mode 100644 index 0000000..f20e898 --- /dev/null +++ b/lib/util/options_extensions.dart @@ -0,0 +1,12 @@ +import 'package:dio/dio.dart'; + +extension OptionsExtension on Options { + Options appendHeaders(Map additionalHeaders) { + if (this.headers != null) { + this.headers?.addAll(additionalHeaders); + } else { + this.headers = additionalHeaders; + } + return this; + } +} \ No newline at end of file diff --git a/lib/util/util.dart b/lib/util/util.dart new file mode 100644 index 0000000..6a35e6c --- /dev/null +++ b/lib/util/util.dart @@ -0,0 +1,4 @@ +Iterable get positiveIntegers sync* { + int i = 0; + while (true) yield i++; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 227b4f4..15be8d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,14 @@ dependencies: quiver: 3.0.1 + dio: 4.0.0 + + json_annotation: 4.0.1 + build_runner: 2.0.5 + json_serializable: 4.1.3 + + built_collection: ^5.1.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/jmap/core/id_test.dart b/test/jmap/core/id_test.dart new file mode 100644 index 0000000..007a55e --- /dev/null +++ b/test/jmap/core/id_test.dart @@ -0,0 +1,27 @@ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; + +void main() { + group('invalid id', () { + test('should throw argument error when the id is empty', () { + expect(() => Id(''), throwsArgumentError); + }); + + test('should throw argument error when the id has length more than 255', () { + expect( + () => Id('a123bde34588799922229339933933933993939393939222a123bde34' + '588799922229339933933933993939393939222a123be3458879992222933993393393' + '3993939393939222a123bde34588799922229339933933933993939393939222a123bd' + 'e34588799922229339933933933993939393939222a123bde345887999222293399339' + '33933993939393939222a123bde34588799922229339933933933993939393939222a1' + '23bde34588799922229339933933933993939393939222'), + throwsArgumentError + ); + }); + + test('should throw argument error when the id start with dash', () { + expect(() => Id('_abe23abc'), throwsArgumentError); + }); + }); +} \ No newline at end of file diff --git a/test/jmap/core/properties/properties_test.dart b/test/jmap/core/properties/properties_test.dart new file mode 100644 index 0000000..bf4c2a0 --- /dev/null +++ b/test/jmap/core/properties/properties_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; + +void main() { + group('test union', () { + test('empty properties should union other properties', () { + final emptyProperties = Properties.empty(); + + expect( + emptyProperties.union(Properties({'id', 'name'})), + equals(Properties({'id', 'name'})) + ); + }); + + test('properties should union other properties', () { + final properties = Properties({'role'}); + final unionProperties = properties.union(Properties({'id', 'name'})); + + expect( + unionProperties, + equals(Properties({'role', 'id', 'name'})) + ); + }); + }); + + group('test removeAll', () { + test('empty properties should not remove anything', () { + final emptyProperties = Properties.empty(); + + expect( + emptyProperties.removeAll(Properties({'id', 'name'})), + equals(emptyProperties) + ); + }); + + test('two-item properties should remove all item when remove three-item properties', () { + final twoItem = Properties({ + 'id', + 'name' + }); + + expect( + twoItem.removeAll(Properties({'id', 'name', 'role'})), + equals(Properties.empty()) + ); + }); + + test('properties can remove all contained items', () { + final properties = Properties({ + 'id', + 'name', + 'role' + }); + + expect( + properties.removeAll(Properties({'id', 'name'})), + equals(Properties({'role'})) + ); + }); + }); +} \ No newline at end of file