From 7ecb191b8f8716c82ffce6be60b81b688b6d7291 Mon Sep 17 00:00:00 2001
From: Semyon Krestyaninov <krestyaninov@perx.ru>
Date: Thu, 19 Dec 2024 10:04:40 +0000
Subject: [PATCH] =?UTF-8?q?chore:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?=
 =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20Taskfile?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Issue: #2849
---
 .gitignore                             |   1 +
 CHANGELOG.md                           |   2 -
 Taskfile.yaml                          | 231 ++++++++--
 cliff.toml                             |  37 +-
 id/mocks/Descriptor.go                 | 157 +++++++
 id/mocks/ObjectHandler.go              |  47 ++
 id/mocks/ObjectIdentifier.go           |  47 ++
 images/mocks/EncodeFunc.go             |  47 ++
 images/mocks/spaceGetter.go            |  42 ++
 logs/mocks/Middleware.go               |  48 +++
 logs/mocks/WriteSyncer.go              |  63 +++
 logs/mocks/spaceGetter.go              |  42 ++
 pkg/clients/mocks/spaceGetter.go       |  42 ++
 pkg/collaborators/mocks/spaceGetter.go |  42 ++
 pkg/delivery/mocks/spaceGetter.go      |  42 ++
 pkg/files/mocks/spaceGetter.go         |  42 ++
 pkg/invitations/mocks/spaceGetter.go   |  42 ++
 pkg/members/mocks/spaceGetter.go       |  42 ++
 pkg/operation/mocks/Service.go         | 144 +++++++
 pkg/references/mocks/spaceGetter.go    |  42 ++
 proto/mailbox/mailbox.pb.go            | 567 +++++++++++++++++++++++++
 proto/mailbox/mailbox_grpc.pb.go       | 208 +++++++++
 proto/mocks/MailboxClient.go           | 144 +++++++
 proto/mocks/MailboxServer.go           | 126 ++++++
 proto/mocks/UnsafeMailboxServer.go     |  29 ++
 25 files changed, 2215 insertions(+), 61 deletions(-)
 create mode 100644 id/mocks/Descriptor.go
 create mode 100644 id/mocks/ObjectHandler.go
 create mode 100644 id/mocks/ObjectIdentifier.go
 create mode 100644 images/mocks/EncodeFunc.go
 create mode 100644 images/mocks/spaceGetter.go
 create mode 100644 logs/mocks/Middleware.go
 create mode 100644 logs/mocks/WriteSyncer.go
 create mode 100644 logs/mocks/spaceGetter.go
 create mode 100644 pkg/clients/mocks/spaceGetter.go
 create mode 100644 pkg/collaborators/mocks/spaceGetter.go
 create mode 100644 pkg/delivery/mocks/spaceGetter.go
 create mode 100644 pkg/files/mocks/spaceGetter.go
 create mode 100644 pkg/invitations/mocks/spaceGetter.go
 create mode 100644 pkg/members/mocks/spaceGetter.go
 create mode 100644 pkg/operation/mocks/Service.go
 create mode 100644 pkg/references/mocks/spaceGetter.go
 create mode 100644 proto/mailbox/mailbox.pb.go
 create mode 100644 proto/mailbox/mailbox_grpc.pb.go
 create mode 100644 proto/mocks/MailboxClient.go
 create mode 100644 proto/mocks/MailboxServer.go
 create mode 100644 proto/mocks/UnsafeMailboxServer.go

diff --git a/.gitignore b/.gitignore
index 34142fe5..e7940660 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 .idea
+.task
 dist/
 release/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 919b98cf..cf3f0b46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,5 @@
 # Changelog
 
-All notable changes to this project will be documented in this file.
-
 ## [0.30.0] - 2024-10-22
 
 ### рџљЂ Features
diff --git a/Taskfile.yaml b/Taskfile.yaml
index a75ab1d8..4a97ef91 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -2,67 +2,212 @@ version: '3'
 
 vars:
   PROTODIR: perxis-proto/proto
-  PBDIR: pb
-  CURRENT_VERSION:
-    sh: svu current
-  RELEASE_VERSION:
-    sh: svu next
+  PBDIR: proto
+  DATE:
+    sh: date -Idate
 
 tasks:
-  changelog:
+  tools:brew:
+    desc: Установка инструментов через brew
     cmds:
-      - git-cliff > CHANGELOG.md --tag {{ .RELEASE_VERSION }}
+      - brew tap caarlos0/tap
+      - brew install caarlos0/tap/svu
+      - brew install git-cliff
+      - brew install goreleaser
+      - brew install mockery
+      - brew install protoc-gen-go
+      - brew install protoc-gen-go-grpc
 
-# release
-# - Сделать changelog
-# - Закоммитить все изменения
-# - Пометить тэгом версию
-#   пререлиз - `git tag "$(svu pr --pre-release alpha.1 --build 9)"`
-#   пререлиз - `git tag "$(svu next)"`
-# - Запушить код и тэги на сервер (иначе будет непонятная ошибка goreleaser Not found)
-  release:
+  # Тесты
+
+  test:unit:
+    aliases: [tu]
+    desc: Запускает только юнит-тесты
+    cmds:
+      - go test ./...
+
+  test:unit:short:
+    aliases: [tus]
+    desc: Запускает только быстрые юнит-тесты
+    cmds:
+      - go test -short ./...
+
+  test:integration:
+    desc: Запускает только интеграционные тесты
+    cmds:
+      - go test -tags integration ./...
+
+  test:integration:short:
+    desc: Запускает только быстрые интеграционные тесты
+    cmds:
+      - go test -tags integration -short ./...
+
+  test:all:
+    desc: Запускает все тесты (юнит и интеграционные)
+    cmds:
+      - go test -tags all ./...
+
+  test:all:short:
+    desc: Запускает все быстрые тесты (юнит и интеграционные)
+    cmds:
+      - go test -tags all -short ./...
+
+  # Release
+
+  release:changelog:
+    aliases: [cl]
+    desc: "Подготовка CHANGELOG.md"
+    vars:
+      CURRENT_VERSION:
+        sh: svu current
+      RELEASE_VERSION:
+        sh: svu next
     cmds:
       - mkdir -p release
-      - git-cliff {{ .CURRENT_VERSION }}.. --tag {{ .RELEASE_VERSION }}  > release/CHANGELOG.md
+      - git cliff {{ .CURRENT_VERSION }}.. --tag {{ .RELEASE_VERSION }} > release/CHANGELOG.md
+
+  release:prepare:
+    desc: "Подготовка к релизу"
+    cmds:
+      - task: release:changelog
+      - go mod tidy
+#      - sed -i '' 's/ServerVersion.*=.*/ServerVersion = "{{ .RELEASE_VERSION }}"/' internal/version/version.go
+#      - sed -i '' 's/APIVersion.*=.*/APIVersion = "{{ .PERXIS_GO_VERSION }}"/' internal/version/version.go
+
+  release:finalize:
+    desc: "Формирует все необходимые файлы к релизу"
+    vars:
+      CURRENT_VERSION:
+        sh: svu current
+      RELEASE_VERSION:
+        sh: svu next
+      MAJOR_VERSION:
+        sh: VERSION="{{ .RELEASE_VERSION }}"; echo "${VERSION%.*}"
+    cmds:
+      - cp CHANGELOG.md CHANGELOG.md.bak
+      - sed -i '' '/^# Changelog/d' CHANGELOG.md
+      - cat release/CHANGELOG.md CHANGELOG.md > CHANGELOG.md.tmp
+      - mv CHANGELOG.md.tmp CHANGELOG.md
+
+
+  release:version:
+    aliases: [v, ver]
+    desc: Текущие версии релизов
+    silent: true
+    vars:
+      CURRENT_VERSION:
+        sh: svu current
+      RELEASE_VERSION:
+        sh: svu next
+    cmds:
+      - echo 'Current version {{.CURRENT_VERSION}}'
+      - echo 'Release version {{.RELEASE_VERSION}}'
+
+  release:build:
+    desc: РЎР±РѕСЂРєР° Release
+    summary: |
+      1. Сделать подготовку:
+        - task release:prepare
+      2. Закоммитить все изменения
+      3. Пометить тэгом версию
+        - пререлиз - `git tag "$(svu pr --pre-release alpha.1 --build 9)"`
+        - релиз - `git tag "$(svu next)"`
+      4. Запушить код и тэги на сервер (иначе будет непонятная ошибка goreleaser Not found)
+    vars:
+      CURRENT_VERSION:
+        sh: svu current
+      RELEASE_VERSION:
+        sh: svu next
+    cmds:
       - goreleaser release --clean --release-notes=release/CHANGELOG.md
 
-  mocks:
-    deps:
-      - mocks.proto
-  mocks.proto:
+  gen:proto:
+    desc: "Компилиция .proto файлов в каталоге {{ .PROTODIR }} в код Go"
     sources:
-      - proto/**/*.proto
-    generates:
-      - proto/mocks/*.go
+      - "{{ .PROTODIR }}/**/*.proto"
+      - exclude: "{{ .PROTODIR }}/status/status.proto"
+    cmds:
+      - for: sources
+        task: gen:proto:file
+        vars:
+          FILE: "{{ .ITEM }}"
+        silent: true
+
+  gen:proto:file:
+    desc: "Преобразует переданный .proto файл в код на языке Go для работы с сообщениями protobuf и сервисами gRPC."
     cmds:
-      - mockery --all --dir proto --output proto/mocks
+      - protoc -I={{ .PROTODIR }} --experimental_allow_proto3_optional --go_out={{ .PBDIR }} --go-grpc_out={{ .PBDIR }} --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative {{ .FILE }}
+
 
+  gen:middleware:
+    desc: "Генерирует middleware для всех пакетов"
+    summary: |
+      Команда перегенерирует middleware для всех пакетов.
 
-  proto:
+      Для конфигурации доступны следующие переменные:
+        * DIR - путь до пакета, содержащего уже сгенерированные middleware
+        * MIDDLEWARE_NAME - имя middleware, которое нужно перегенерировать, без указания расширения
+
+      Примеры использования:
+        Перегенерация всех middleware только для пакета items:
+          task regen:middleware DIR=pkg/items
+
+        Перегенерация всех recovery middleware для всех пакетов:
+          task regen:middleware MIDDLEWARE_NAME=recovering_middleware
+
+        Перегенерация middleware телеметрии для пакета collections:
+          task gen:middleware DIR=pkg/collections MIDDLEWARE_NAME=recovering_middleware
+    vars:
+      DIR: '{{ default "**" .DIR }}'
+      MIDDLEWARE_NAME: '{{ default "*" .MIDDLEWARE_NAME }}'
     sources:
-     - '{{ .PROTODIR }}/**/*.proto'
-#    generates:
-#      - '{{ .PBDIR }}/*.go'
-    ignore_error: true # Игнорировать ошибки, из-за status/status.proto
-#    silent: true
+      - "{{ .DIR }}/middleware/{{ .MIDDLEWARE_NAME }}.go"
     cmds:
       - for: sources
-        cmd: echo {{ .ITEM }}
-#        cmd: '[ "{{.FILE}}" != "perxis-proto/proto/status/status.proto" ]'
-#          - protoc --proto_path={{ .PROTODIR }} --experimental_allow_proto3_optional --go_out={{ .PBDIR }} --go-grpc_out={{ .PBDIR }} --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative {{ .FILE }}
+        cmd: go generate {{ .ITEM }}
 
+  gen:mocks:
+    desc: "Генерирует моки"
+    cmds:
+      - task: gen:mocks:proto
+      - task: gen:mocks:services
 
-  #        cmd: protoc --proto_path={{ .PROTODIR }} --experimental_allow_proto3_optional --go_out={{ .PBDIR }} --go-grpc_out={{ .PBDIR }} --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative {{ .ITEM }}
-#        task: proto_file
-#        vars:
-#          FILE: '{{ .ITEM }}'
-#        ignore_error: true
+  gen:mocks:proto:
+    desc: "Команда генерирует моки для всех proto-файлов"
+    sources:
+      - "{{.PBDIR}}/**/*.pb.go"
+    cmds:
+      - mockery --all --dir {{.PBDIR}} --output {{.PBDIR}}/mocks
+
+
+  gen:mocks:services:
+    desc: "Генерирует моки для всех интерфейсов в директориях, где находится файл service.go."
+    sources:
+      - "**/service.go"
+      - exclude: "**/mocks/**"
+    cmds:
+      - for: sources
+        task: gen:mock:dir
+        vars:
+          DIR: "{{ dir .ITEM }}"
 
+  gen:mock:dir:
+    internal: true
+    desc: "Вспомогательная команда для генерации моков"
+    cmds:
+      - mockery --log-level="error" --all --exported --dir "{{ .DIR }}" --output="{{ .DIR }}/mocks"
 
-  proto_file:
+  gen:microgen:all:
+    desc: "Генерирует go-kit для всех файлов service.go. Поддерживается для версии microgen < 1.0.0"
     sources:
-      - '{{ .FILE }}'
+      - "**/service.go"
+    deps:
+      - for: sources
+        task: gen:microgen:file
+        vars:
+          FILE: "{{ .ITEM }}"
+
+  gen:microgen:file:
+    desc: "Вспомогательная команда для генерации go-kit для переданного файла. Выходные файлы сохраняются в директории с входным файлом."
     cmds:
-      - '[ "{{.FILE}}" != "perxis-proto/proto/status/status.proto" ]' # Игнорировать ошибки, из-за status/status.proto
-      - protoc --proto_path={{ .PROTODIR }} --experimental_allow_proto3_optional --go_out={{ .PBDIR }} --go-grpc_out={{ .PBDIR }} --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative {{ .FILE }}
-    silent: true
\ No newline at end of file
+      - microgen -file {{ .FILE }} -out {{ dir .FILE  }}
diff --git a/cliff.toml b/cliff.toml
index 78b82d95..572bb688 100644
--- a/cliff.toml
+++ b/cliff.toml
@@ -12,7 +12,6 @@ breaking_always_bump_major = true
 # changelog header
 header = """
 # Changelog\n
-All notable changes to this project will be documented in this file.\n
 """
 # template for the changelog body
 # https://keats.github.io/tera/docs/#introduction
@@ -25,16 +24,16 @@ body = """
 {% for group, commits in commits | group_by(attribute="group") %}
     ### {{ group | striptags | trim | upper_first }}
     {% for commit in commits %}
-        - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
+        - {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\
             {% if commit.breaking %}[**breaking**] {% endif %}\
             {{ commit.message | upper_first }} \
         {% if commit.links %}\
-       ({% for link in commit.links %}\
-          [{{ link.text }}]({{ link.href }})\
+       {% for link in commit.links %}\
+          [{{ link.text }}]({{ link.href }}) \
           {% if not loop.last %}, {% endif %}\
-        {% endfor %})\
+        {% endfor %}\
         {% endif %}\
-        -([{{ commit.id | truncate(length=7, end="") }}]($REPO/-/commit/{{ commit.id }}))\
+        ([{{ commit.id | truncate(length=7, end="") }}]($REPO/-/commit/{{ commit.id }}))\
     {% endfor %}
 {% endfor %}\n
 """
@@ -69,20 +68,20 @@ commit_preprocessors = [
 ]
 # regex for parsing and grouping commits
 commit_parsers = [
-    { message = "^feat", group = "<!-- 0 -->рџљЂ Features" },
-    { message = "^fix", group = "<!-- 1 -->рџђ› Bug Fixes" },
-    { message = "^doc", group = "<!-- 3 -->рџ“љ Documentation" },
-    { message = "^perf", group = "<!-- 4 -->вљЎ Performance" },
-    { message = "^refactor", group = "<!-- 2 -->рџљњ Refactor", skip = true },
+    { message = "^feat", group = "<!-- 0 -->🚀 Новые возможности" },
+    { message = "^fix", group = "<!-- 1 -->🐛 Исправлены ошибки" },
+    { message = "^doc", group = "<!-- 3 -->📚 Документация" },
+    { message = "^perf", group = "<!-- 4 -->⚡ Производительность" },
+    { message = "^refactor", group = "<!-- 2 -->🚜 Рефакторинг", skip = true },
     { message = "^style", group = "<!-- 5 -->рџЋЁ Styling" },
-    { message = "^test", group = "<!-- 6 -->рџ§Є Testing" },
+    { message = "^test", group = "<!-- 6 -->🧪 Тесты" },
     { message = "^chore\\(release\\): prepare for", skip = true },
     { message = "^chore\\(deps.*\\)", skip = true },
     { message = "^chore\\(pr\\)", skip = true },
     { message = "^chore\\(pull\\)", skip = true },
-    { message = "^chore|^ci", group = "<!-- 7 -->вљ™пёЏ Miscellaneous Tasks" },
-    { body = ".*security", group = "<!-- 8 -->рџ›ЎпёЏ Security" },
-    { message = "^revert", group = "<!-- 9 -->в—ЂпёЏ Revert" },
+    { message = "^chore|^ci", group = "<!-- 7 -->⚙️ Прочие задачи" },
+    { body = ".*security", group = "<!-- 8 -->🛡️ Безопастность" },
+    { message = "^revert", group = "<!-- 9 -->◀️ Отменены изменения" },
     { message = "^irefac", skip = true },
 ]
 # protect breaking changes from being skipped due to matching a skipping commit_parser
@@ -102,6 +101,12 @@ sort_commits = "oldest"
 # limit the number of commits included in the changelog.
 # limit_commits = 42
 link_parsers = [
+    { pattern = "#(\\d+)", href = "https://git.perx.ru/perxis/perxis/-/issues/$1"},
     { pattern = "#(PRXS-(\\d+))", href = "https://tracker.yandex.ru/$1"},
     { pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"},
-]
\ No newline at end of file
+]
+
+[remote.gitlab]
+owner = "perxis"
+repo = "perxis-go"
+api_url = "https://git.perx.ru/api/v4/"
\ No newline at end of file
diff --git a/id/mocks/Descriptor.go b/id/mocks/Descriptor.go
new file mode 100644
index 00000000..10a6b090
--- /dev/null
+++ b/id/mocks/Descriptor.go
@@ -0,0 +1,157 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	id "git.perx.ru/perxis/perxis-go/id"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Descriptor is an autogenerated mock type for the Descriptor type
+type Descriptor struct {
+	mock.Mock
+}
+
+// FromMap provides a mock function with given fields: _a0
+func (_m *Descriptor) FromMap(_a0 map[string]interface{}) error {
+	ret := _m.Called(_a0)
+
+	if len(ret) == 0 {
+		panic("no return value specified for FromMap")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(map[string]interface{}) error); ok {
+		r0 = rf(_a0)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// FromParts provides a mock function with given fields: _a0
+func (_m *Descriptor) FromParts(_a0 []string) error {
+	ret := _m.Called(_a0)
+
+	if len(ret) == 0 {
+		panic("no return value specified for FromParts")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func([]string) error); ok {
+		r0 = rf(_a0)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Map provides a mock function with no fields
+func (_m *Descriptor) Map() map[string]interface{} {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for Map")
+	}
+
+	var r0 map[string]interface{}
+	if rf, ok := ret.Get(0).(func() map[string]interface{}); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(map[string]interface{})
+		}
+	}
+
+	return r0
+}
+
+// New provides a mock function with no fields
+func (_m *Descriptor) New() id.Descriptor {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for New")
+	}
+
+	var r0 id.Descriptor
+	if rf, ok := ret.Get(0).(func() id.Descriptor); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(id.Descriptor)
+		}
+	}
+
+	return r0
+}
+
+// String provides a mock function with no fields
+func (_m *Descriptor) String() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for String")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// Type provides a mock function with no fields
+func (_m *Descriptor) Type() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for Type")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// Validate provides a mock function with no fields
+func (_m *Descriptor) Validate() error {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for Validate")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// NewDescriptor creates a new instance of Descriptor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewDescriptor(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *Descriptor {
+	mock := &Descriptor{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/id/mocks/ObjectHandler.go b/id/mocks/ObjectHandler.go
new file mode 100644
index 00000000..8e587abf
--- /dev/null
+++ b/id/mocks/ObjectHandler.go
@@ -0,0 +1,47 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	id "git.perx.ru/perxis/perxis-go/id"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// ObjectHandler is an autogenerated mock type for the ObjectHandler type
+type ObjectHandler struct {
+	mock.Mock
+}
+
+// Execute provides a mock function with given fields: _a0
+func (_m *ObjectHandler) Execute(_a0 interface{}) *id.ObjectId {
+	ret := _m.Called(_a0)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Execute")
+	}
+
+	var r0 *id.ObjectId
+	if rf, ok := ret.Get(0).(func(interface{}) *id.ObjectId); ok {
+		r0 = rf(_a0)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*id.ObjectId)
+		}
+	}
+
+	return r0
+}
+
+// NewObjectHandler creates a new instance of ObjectHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewObjectHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *ObjectHandler {
+	mock := &ObjectHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/id/mocks/ObjectIdentifier.go b/id/mocks/ObjectIdentifier.go
new file mode 100644
index 00000000..9a7ba02c
--- /dev/null
+++ b/id/mocks/ObjectIdentifier.go
@@ -0,0 +1,47 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	id "git.perx.ru/perxis/perxis-go/id"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// ObjectIdentifier is an autogenerated mock type for the ObjectIdentifier type
+type ObjectIdentifier struct {
+	mock.Mock
+}
+
+// ObjectId provides a mock function with no fields
+func (_m *ObjectIdentifier) ObjectId() *id.ObjectId {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for ObjectId")
+	}
+
+	var r0 *id.ObjectId
+	if rf, ok := ret.Get(0).(func() *id.ObjectId); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*id.ObjectId)
+		}
+	}
+
+	return r0
+}
+
+// NewObjectIdentifier creates a new instance of ObjectIdentifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewObjectIdentifier(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *ObjectIdentifier {
+	mock := &ObjectIdentifier{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/images/mocks/EncodeFunc.go b/images/mocks/EncodeFunc.go
new file mode 100644
index 00000000..2a31b5ff
--- /dev/null
+++ b/images/mocks/EncodeFunc.go
@@ -0,0 +1,47 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	image "image"
+	io "io"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// EncodeFunc is an autogenerated mock type for the EncodeFunc type
+type EncodeFunc struct {
+	mock.Mock
+}
+
+// Execute provides a mock function with given fields: w, img
+func (_m *EncodeFunc) Execute(w io.Writer, img image.Image) error {
+	ret := _m.Called(w, img)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Execute")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(io.Writer, image.Image) error); ok {
+		r0 = rf(w, img)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// NewEncodeFunc creates a new instance of EncodeFunc. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewEncodeFunc(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *EncodeFunc {
+	mock := &EncodeFunc{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/images/mocks/spaceGetter.go b/images/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/images/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/logs/mocks/Middleware.go b/logs/mocks/Middleware.go
new file mode 100644
index 00000000..1cf8d39f
--- /dev/null
+++ b/logs/mocks/Middleware.go
@@ -0,0 +1,48 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	logs "git.perx.ru/perxis/perxis-go/logs"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Middleware is an autogenerated mock type for the Middleware type
+type Middleware struct {
+	mock.Mock
+}
+
+// Execute provides a mock function with given fields: _a0
+func (_m *Middleware) Execute(_a0 logs.Service) logs.Service {
+	ret := _m.Called(_a0)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Execute")
+	}
+
+	var r0 logs.Service
+	if rf, ok := ret.Get(0).(func(logs.Service) logs.Service); ok {
+		r0 = rf(_a0)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(logs.Service)
+		}
+	}
+
+	return r0
+}
+
+// NewMiddleware creates a new instance of Middleware. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMiddleware(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *Middleware {
+	mock := &Middleware{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/logs/mocks/WriteSyncer.go b/logs/mocks/WriteSyncer.go
new file mode 100644
index 00000000..70b9414d
--- /dev/null
+++ b/logs/mocks/WriteSyncer.go
@@ -0,0 +1,63 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	logs "git.perx.ru/perxis/perxis-go/logs"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// WriteSyncer is an autogenerated mock type for the WriteSyncer type
+type WriteSyncer struct {
+	mock.Mock
+}
+
+// Sync provides a mock function with no fields
+func (_m *WriteSyncer) Sync() error {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for Sync")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Write provides a mock function with given fields: entry
+func (_m *WriteSyncer) Write(entry *logs.Entry) error {
+	ret := _m.Called(entry)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Write")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(*logs.Entry) error); ok {
+		r0 = rf(entry)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// NewWriteSyncer creates a new instance of WriteSyncer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewWriteSyncer(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *WriteSyncer {
+	mock := &WriteSyncer{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/logs/mocks/spaceGetter.go b/logs/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/logs/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/clients/mocks/spaceGetter.go b/pkg/clients/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/clients/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/collaborators/mocks/spaceGetter.go b/pkg/collaborators/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/collaborators/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/delivery/mocks/spaceGetter.go b/pkg/delivery/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/delivery/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/files/mocks/spaceGetter.go b/pkg/files/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/files/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/invitations/mocks/spaceGetter.go b/pkg/invitations/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/invitations/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/members/mocks/spaceGetter.go b/pkg/members/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/members/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/operation/mocks/Service.go b/pkg/operation/mocks/Service.go
new file mode 100644
index 00000000..578f6d05
--- /dev/null
+++ b/pkg/operation/mocks/Service.go
@@ -0,0 +1,144 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	operation "git.perx.ru/perxis/perxis-go/pkg/operation"
+	mock "github.com/stretchr/testify/mock"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// Service is an autogenerated mock type for the Service type
+type Service struct {
+	mock.Mock
+}
+
+// Cancel provides a mock function with given fields: ctx, id
+func (_m *Service) Cancel(ctx context.Context, id string) (*operation.Operation, error) {
+	ret := _m.Called(ctx, id)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Cancel")
+	}
+
+	var r0 *operation.Operation
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) (*operation.Operation, error)); ok {
+		return rf(ctx, id)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string) *operation.Operation); ok {
+		r0 = rf(ctx, id)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*operation.Operation)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, id)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Cleanup provides a mock function with no fields
+func (_m *Service) Cleanup() {
+	_m.Called()
+}
+
+// Create provides a mock function with given fields: ctx, desc, fn
+func (_m *Service) Create(ctx context.Context, desc string, fn func(context.Context) (protoreflect.ProtoMessage, error)) (*operation.Operation, error) {
+	ret := _m.Called(ctx, desc, fn)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Create")
+	}
+
+	var r0 *operation.Operation
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, func(context.Context) (protoreflect.ProtoMessage, error)) (*operation.Operation, error)); ok {
+		return rf(ctx, desc, fn)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, func(context.Context) (protoreflect.ProtoMessage, error)) *operation.Operation); ok {
+		r0 = rf(ctx, desc, fn)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*operation.Operation)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, func(context.Context) (protoreflect.ProtoMessage, error)) error); ok {
+		r1 = rf(ctx, desc, fn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Get provides a mock function with given fields: ctx, id
+func (_m *Service) Get(ctx context.Context, id string) (*operation.Operation, error) {
+	ret := _m.Called(ctx, id)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Get")
+	}
+
+	var r0 *operation.Operation
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) (*operation.Operation, error)); ok {
+		return rf(ctx, id)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string) *operation.Operation); ok {
+		r0 = rf(ctx, id)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*operation.Operation)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, id)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Set provides a mock function with given fields: ctx, op
+func (_m *Service) Set(ctx context.Context, op *operation.Operation) error {
+	ret := _m.Called(ctx, op)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Set")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *operation.Operation) error); ok {
+		r0 = rf(ctx, op)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewService(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *Service {
+	mock := &Service{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/references/mocks/spaceGetter.go b/pkg/references/mocks/spaceGetter.go
new file mode 100644
index 00000000..b56dbea6
--- /dev/null
+++ b/pkg/references/mocks/spaceGetter.go
@@ -0,0 +1,42 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceGetter is an autogenerated mock type for the spaceGetter type
+type SpaceGetter struct {
+	mock.Mock
+}
+
+// GetSpaceID provides a mock function with no fields
+func (_m *SpaceGetter) GetSpaceID() string {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetSpaceID")
+	}
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// NewSpaceGetter creates a new instance of SpaceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewSpaceGetter(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *SpaceGetter {
+	mock := &SpaceGetter{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/proto/mailbox/mailbox.pb.go b/proto/mailbox/mailbox.pb.go
new file mode 100644
index 00000000..6083aeb2
--- /dev/null
+++ b/proto/mailbox/mailbox.pb.go
@@ -0,0 +1,567 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.35.2
+// 	protoc        v5.29.0
+// source: mailbox/mailbox.proto
+
+package mailbox
+
+import (
+	common "git.perx.ru/perxis/perxis-go/proto/common"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Сообщение для уведомления
+type Message struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Id        string                 `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`                                // Уникальный идентификатор уведомления
+	ObjectId  string                 `protobuf:"bytes,2,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"`    // Идентификатор объекта события
+	From      string                 `protobuf:"bytes,3,opt,name=from,proto3" json:"from,omitempty"`                            // Идентификатор отправителя пользователя/сервиса/подсистемы
+	To        string                 `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"`                                // Идентификатор получателя пользователя/сервиса/подсистемы
+	Title     string                 `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"`                          // Заголовок уведомления
+	Message   string                 `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"`                      // Текст уведомления
+	CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Время создания
+	IsRead    bool                   `protobuf:"varint,8,opt,name=is_read,json=isRead,proto3" json:"is_read,omitempty"`         // Статус прочтения
+}
+
+func (x *Message) Reset() {
+	*x = Message{}
+	mi := &file_mailbox_mailbox_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Message) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Message) ProtoMessage() {}
+
+func (x *Message) ProtoReflect() protoreflect.Message {
+	mi := &file_mailbox_mailbox_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Message.ProtoReflect.Descriptor instead.
+func (*Message) Descriptor() ([]byte, []int) {
+	return file_mailbox_mailbox_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Message) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+func (x *Message) GetObjectId() string {
+	if x != nil {
+		return x.ObjectId
+	}
+	return ""
+}
+
+func (x *Message) GetFrom() string {
+	if x != nil {
+		return x.From
+	}
+	return ""
+}
+
+func (x *Message) GetTo() string {
+	if x != nil {
+		return x.To
+	}
+	return ""
+}
+
+func (x *Message) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *Message) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+func (x *Message) GetCreatedAt() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreatedAt
+	}
+	return nil
+}
+
+func (x *Message) GetIsRead() bool {
+	if x != nil {
+		return x.IsRead
+	}
+	return false
+}
+
+// Отправить уведомление
+type SendMessageRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	To      []string `protobuf:"bytes,1,rep,name=to,proto3" json:"to,omitempty"` // список получателей пользователей/сервисов/подсистем
+	Message *Message `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *SendMessageRequest) Reset() {
+	*x = SendMessageRequest{}
+	mi := &file_mailbox_mailbox_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SendMessageRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendMessageRequest) ProtoMessage() {}
+
+func (x *SendMessageRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_mailbox_mailbox_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendMessageRequest.ProtoReflect.Descriptor instead.
+func (*SendMessageRequest) Descriptor() ([]byte, []int) {
+	return file_mailbox_mailbox_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *SendMessageRequest) GetTo() []string {
+	if x != nil {
+		return x.To
+	}
+	return nil
+}
+
+func (x *SendMessageRequest) GetMessage() *Message {
+	if x != nil {
+		return x.Message
+	}
+	return nil
+}
+
+// Запрос на получение уведомлений
+type ListMessageRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Options *FindOptions `protobuf:"bytes,10,opt,name=options,proto3" json:"options,omitempty"`
+}
+
+func (x *ListMessageRequest) Reset() {
+	*x = ListMessageRequest{}
+	mi := &file_mailbox_mailbox_proto_msgTypes[2]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ListMessageRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListMessageRequest) ProtoMessage() {}
+
+func (x *ListMessageRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_mailbox_mailbox_proto_msgTypes[2]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListMessageRequest.ProtoReflect.Descriptor instead.
+func (*ListMessageRequest) Descriptor() ([]byte, []int) {
+	return file_mailbox_mailbox_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *ListMessageRequest) GetOptions() *FindOptions {
+	if x != nil {
+		return x.Options
+	}
+	return nil
+}
+
+// Ответ на получение уведомлений
+type ListMessageResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Message []*Message `protobuf:"bytes,1,rep,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *ListMessageResponse) Reset() {
+	*x = ListMessageResponse{}
+	mi := &file_mailbox_mailbox_proto_msgTypes[3]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ListMessageResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListMessageResponse) ProtoMessage() {}
+
+func (x *ListMessageResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_mailbox_mailbox_proto_msgTypes[3]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListMessageResponse.ProtoReflect.Descriptor instead.
+func (*ListMessageResponse) Descriptor() ([]byte, []int) {
+	return file_mailbox_mailbox_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ListMessageResponse) GetMessage() []*Message {
+	if x != nil {
+		return x.Message
+	}
+	return nil
+}
+
+// Пометка уведомлений как прочитанных
+type MarkMessagesRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Unread  bool         `protobuf:"varint,1,opt,name=unread,proto3" json:"unread,omitempty"` // Установить статус сообщения не/прочитано
+	Options *FindOptions `protobuf:"bytes,10,opt,name=options,proto3" json:"options,omitempty"`
+}
+
+func (x *MarkMessagesRequest) Reset() {
+	*x = MarkMessagesRequest{}
+	mi := &file_mailbox_mailbox_proto_msgTypes[4]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *MarkMessagesRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MarkMessagesRequest) ProtoMessage() {}
+
+func (x *MarkMessagesRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_mailbox_mailbox_proto_msgTypes[4]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MarkMessagesRequest.ProtoReflect.Descriptor instead.
+func (*MarkMessagesRequest) Descriptor() ([]byte, []int) {
+	return file_mailbox_mailbox_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *MarkMessagesRequest) GetUnread() bool {
+	if x != nil {
+		return x.Unread
+	}
+	return false
+}
+
+func (x *MarkMessagesRequest) GetOptions() *FindOptions {
+	if x != nil {
+		return x.Options
+	}
+	return nil
+}
+
+type FindOptions struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Recipient  string                 `protobuf:"bytes,1,opt,name=recipient,proto3" json:"recipient,omitempty"`
+	MessageIds []string               `protobuf:"bytes,3,rep,name=message_ids,json=messageIds,proto3" json:"message_ids,omitempty"`
+	After      *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=after,proto3" json:"after,omitempty"`
+	Before     *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=before,proto3" json:"before,omitempty"`
+	Unread     *bool                  `protobuf:"varint,6,opt,name=unread,proto3,oneof" json:"unread,omitempty"`
+	Options    *common.FindOptions    `protobuf:"bytes,10,opt,name=options,proto3" json:"options,omitempty"`
+}
+
+func (x *FindOptions) Reset() {
+	*x = FindOptions{}
+	mi := &file_mailbox_mailbox_proto_msgTypes[5]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *FindOptions) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindOptions) ProtoMessage() {}
+
+func (x *FindOptions) ProtoReflect() protoreflect.Message {
+	mi := &file_mailbox_mailbox_proto_msgTypes[5]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindOptions.ProtoReflect.Descriptor instead.
+func (*FindOptions) Descriptor() ([]byte, []int) {
+	return file_mailbox_mailbox_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *FindOptions) GetRecipient() string {
+	if x != nil {
+		return x.Recipient
+	}
+	return ""
+}
+
+func (x *FindOptions) GetMessageIds() []string {
+	if x != nil {
+		return x.MessageIds
+	}
+	return nil
+}
+
+func (x *FindOptions) GetAfter() *timestamppb.Timestamp {
+	if x != nil {
+		return x.After
+	}
+	return nil
+}
+
+func (x *FindOptions) GetBefore() *timestamppb.Timestamp {
+	if x != nil {
+		return x.Before
+	}
+	return nil
+}
+
+func (x *FindOptions) GetUnread() bool {
+	if x != nil && x.Unread != nil {
+		return *x.Unread
+	}
+	return false
+}
+
+func (x *FindOptions) GetOptions() *common.FindOptions {
+	if x != nil {
+		return x.Options
+	}
+	return nil
+}
+
+var File_mailbox_mailbox_proto protoreflect.FileDescriptor
+
+var file_mailbox_mailbox_proto_rawDesc = []byte{
+	0x0a, 0x15, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2f, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f,
+	0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x70, 0x65, 0x72, 0x78, 0x69, 0x73, 0x2e,
+	0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d,
+	0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
+	0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xde, 0x01, 0x0a, 0x07, 0x4d,
+	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74,
+	0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x62, 0x6a, 0x65, 0x63,
+	0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x18, 0x0a,
+	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
+	0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
+	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
+	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
+	0x41, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x18, 0x08, 0x20,
+	0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x52, 0x65, 0x61, 0x64, 0x22, 0x57, 0x0a, 0x12, 0x53,
+	0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x02, 0x74,
+	0x6f, 0x12, 0x31, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x72, 0x78, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c,
+	0x62, 0x6f, 0x78, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x22, 0x4b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x07, 0x6f, 0x70,
+	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x65,
+	0x72, 0x78, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x46, 0x69, 0x6e,
+	0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+	0x73, 0x22, 0x48, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x72, 0x78,
+	0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61,
+	0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x64, 0x0a, 0x13, 0x4d,
+	0x61, 0x72, 0x6b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x12, 0x35, 0x0a, 0x07, 0x6f, 0x70,
+	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x65,
+	0x72, 0x78, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x46, 0x69, 0x6e,
+	0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+	0x73, 0x22, 0x89, 0x02, 0x0a, 0x0b, 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+	0x73, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12,
+	0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03,
+	0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x64, 0x73,
+	0x12, 0x30, 0x0a, 0x05, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+	0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x61, 0x66, 0x74,
+	0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06,
+	0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64,
+	0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64,
+	0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0a,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x69,
+	0x6e, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+	0x6e, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x32, 0xed, 0x01,
+	0x0a, 0x07, 0x4d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x12, 0x44, 0x0a, 0x04, 0x53, 0x65, 0x6e,
+	0x64, 0x12, 0x22, 0x2e, 0x70, 0x65, 0x72, 0x78, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62,
+	0x6f, 0x78, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
+	0x4f, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x70, 0x65, 0x72, 0x78, 0x69, 0x73,
+	0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x70, 0x65,
+	0x72, 0x78, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x73,
+	0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x12, 0x4b, 0x0a, 0x0a, 0x4d, 0x61, 0x72, 0x6b, 0x41, 0x73, 0x52, 0x65, 0x61, 0x64, 0x12, 0x23,
+	0x2e, 0x70, 0x65, 0x72, 0x78, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x2e,
+	0x4d, 0x61, 0x72, 0x6b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x32, 0x5a,
+	0x30, 0x67, 0x69, 0x74, 0x2e, 0x70, 0x65, 0x72, 0x78, 0x2e, 0x72, 0x75, 0x2f, 0x70, 0x65, 0x72,
+	0x78, 0x69, 0x73, 0x2f, 0x70, 0x65, 0x72, 0x78, 0x69, 0x73, 0x2d, 0x67, 0x6f, 0x2f, 0x61, 0x70,
+	0x69, 0x2f, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x3b, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f,
+	0x78, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_mailbox_mailbox_proto_rawDescOnce sync.Once
+	file_mailbox_mailbox_proto_rawDescData = file_mailbox_mailbox_proto_rawDesc
+)
+
+func file_mailbox_mailbox_proto_rawDescGZIP() []byte {
+	file_mailbox_mailbox_proto_rawDescOnce.Do(func() {
+		file_mailbox_mailbox_proto_rawDescData = protoimpl.X.CompressGZIP(file_mailbox_mailbox_proto_rawDescData)
+	})
+	return file_mailbox_mailbox_proto_rawDescData
+}
+
+var file_mailbox_mailbox_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
+var file_mailbox_mailbox_proto_goTypes = []any{
+	(*Message)(nil),               // 0: perxis.mailbox.Message
+	(*SendMessageRequest)(nil),    // 1: perxis.mailbox.SendMessageRequest
+	(*ListMessageRequest)(nil),    // 2: perxis.mailbox.ListMessageRequest
+	(*ListMessageResponse)(nil),   // 3: perxis.mailbox.ListMessageResponse
+	(*MarkMessagesRequest)(nil),   // 4: perxis.mailbox.MarkMessagesRequest
+	(*FindOptions)(nil),           // 5: perxis.mailbox.FindOptions
+	(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
+	(*common.FindOptions)(nil),    // 7: common.FindOptions
+	(*emptypb.Empty)(nil),         // 8: google.protobuf.Empty
+}
+var file_mailbox_mailbox_proto_depIdxs = []int32{
+	6,  // 0: perxis.mailbox.Message.created_at:type_name -> google.protobuf.Timestamp
+	0,  // 1: perxis.mailbox.SendMessageRequest.message:type_name -> perxis.mailbox.Message
+	5,  // 2: perxis.mailbox.ListMessageRequest.options:type_name -> perxis.mailbox.FindOptions
+	0,  // 3: perxis.mailbox.ListMessageResponse.message:type_name -> perxis.mailbox.Message
+	5,  // 4: perxis.mailbox.MarkMessagesRequest.options:type_name -> perxis.mailbox.FindOptions
+	6,  // 5: perxis.mailbox.FindOptions.after:type_name -> google.protobuf.Timestamp
+	6,  // 6: perxis.mailbox.FindOptions.before:type_name -> google.protobuf.Timestamp
+	7,  // 7: perxis.mailbox.FindOptions.options:type_name -> common.FindOptions
+	1,  // 8: perxis.mailbox.Mailbox.Send:input_type -> perxis.mailbox.SendMessageRequest
+	2,  // 9: perxis.mailbox.Mailbox.List:input_type -> perxis.mailbox.ListMessageRequest
+	4,  // 10: perxis.mailbox.Mailbox.MarkAsRead:input_type -> perxis.mailbox.MarkMessagesRequest
+	8,  // 11: perxis.mailbox.Mailbox.Send:output_type -> google.protobuf.Empty
+	3,  // 12: perxis.mailbox.Mailbox.List:output_type -> perxis.mailbox.ListMessageResponse
+	8,  // 13: perxis.mailbox.Mailbox.MarkAsRead:output_type -> google.protobuf.Empty
+	11, // [11:14] is the sub-list for method output_type
+	8,  // [8:11] is the sub-list for method input_type
+	8,  // [8:8] is the sub-list for extension type_name
+	8,  // [8:8] is the sub-list for extension extendee
+	0,  // [0:8] is the sub-list for field type_name
+}
+
+func init() { file_mailbox_mailbox_proto_init() }
+func file_mailbox_mailbox_proto_init() {
+	if File_mailbox_mailbox_proto != nil {
+		return
+	}
+	file_mailbox_mailbox_proto_msgTypes[5].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_mailbox_mailbox_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   6,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_mailbox_mailbox_proto_goTypes,
+		DependencyIndexes: file_mailbox_mailbox_proto_depIdxs,
+		MessageInfos:      file_mailbox_mailbox_proto_msgTypes,
+	}.Build()
+	File_mailbox_mailbox_proto = out.File
+	file_mailbox_mailbox_proto_rawDesc = nil
+	file_mailbox_mailbox_proto_goTypes = nil
+	file_mailbox_mailbox_proto_depIdxs = nil
+}
diff --git a/proto/mailbox/mailbox_grpc.pb.go b/proto/mailbox/mailbox_grpc.pb.go
new file mode 100644
index 00000000..19e54c06
--- /dev/null
+++ b/proto/mailbox/mailbox_grpc.pb.go
@@ -0,0 +1,208 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc             v5.29.0
+// source: mailbox/mailbox.proto
+
+package mailbox
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+	Mailbox_Send_FullMethodName       = "/perxis.mailbox.Mailbox/Send"
+	Mailbox_List_FullMethodName       = "/perxis.mailbox.Mailbox/List"
+	Mailbox_MarkAsRead_FullMethodName = "/perxis.mailbox.Mailbox/MarkAsRead"
+)
+
+// MailboxClient is the client API for Mailbox service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// Сервис уведомлений
+type MailboxClient interface {
+	// Отправка уведомления
+	Send(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	// Получение уведомлений, поле to в Message всегда содержит только получателя из запроса
+	List(ctx context.Context, in *ListMessageRequest, opts ...grpc.CallOption) (*ListMessageResponse, error)
+	// Пометка уведомлений как прочитанных
+	MarkAsRead(ctx context.Context, in *MarkMessagesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+}
+
+type mailboxClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewMailboxClient(cc grpc.ClientConnInterface) MailboxClient {
+	return &mailboxClient{cc}
+}
+
+func (c *mailboxClient) Send(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, Mailbox_Send_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *mailboxClient) List(ctx context.Context, in *ListMessageRequest, opts ...grpc.CallOption) (*ListMessageResponse, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(ListMessageResponse)
+	err := c.cc.Invoke(ctx, Mailbox_List_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *mailboxClient) MarkAsRead(ctx context.Context, in *MarkMessagesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, Mailbox_MarkAsRead_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// MailboxServer is the server API for Mailbox service.
+// All implementations must embed UnimplementedMailboxServer
+// for forward compatibility.
+//
+// Сервис уведомлений
+type MailboxServer interface {
+	// Отправка уведомления
+	Send(context.Context, *SendMessageRequest) (*emptypb.Empty, error)
+	// Получение уведомлений, поле to в Message всегда содержит только получателя из запроса
+	List(context.Context, *ListMessageRequest) (*ListMessageResponse, error)
+	// Пометка уведомлений как прочитанных
+	MarkAsRead(context.Context, *MarkMessagesRequest) (*emptypb.Empty, error)
+	mustEmbedUnimplementedMailboxServer()
+}
+
+// UnimplementedMailboxServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedMailboxServer struct{}
+
+func (UnimplementedMailboxServer) Send(context.Context, *SendMessageRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
+}
+func (UnimplementedMailboxServer) List(context.Context, *ListMessageRequest) (*ListMessageResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
+}
+func (UnimplementedMailboxServer) MarkAsRead(context.Context, *MarkMessagesRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method MarkAsRead not implemented")
+}
+func (UnimplementedMailboxServer) mustEmbedUnimplementedMailboxServer() {}
+func (UnimplementedMailboxServer) testEmbeddedByValue()                 {}
+
+// UnsafeMailboxServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to MailboxServer will
+// result in compilation errors.
+type UnsafeMailboxServer interface {
+	mustEmbedUnimplementedMailboxServer()
+}
+
+func RegisterMailboxServer(s grpc.ServiceRegistrar, srv MailboxServer) {
+	// If the following call pancis, it indicates UnimplementedMailboxServer was
+	// embedded by pointer and is nil.  This will cause panics if an
+	// unimplemented method is ever invoked, so we test this at initialization
+	// time to prevent it from happening at runtime later due to I/O.
+	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+		t.testEmbeddedByValue()
+	}
+	s.RegisterService(&Mailbox_ServiceDesc, srv)
+}
+
+func _Mailbox_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SendMessageRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(MailboxServer).Send(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Mailbox_Send_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(MailboxServer).Send(ctx, req.(*SendMessageRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Mailbox_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListMessageRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(MailboxServer).List(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Mailbox_List_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(MailboxServer).List(ctx, req.(*ListMessageRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Mailbox_MarkAsRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(MarkMessagesRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(MailboxServer).MarkAsRead(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Mailbox_MarkAsRead_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(MailboxServer).MarkAsRead(ctx, req.(*MarkMessagesRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// Mailbox_ServiceDesc is the grpc.ServiceDesc for Mailbox service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Mailbox_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "perxis.mailbox.Mailbox",
+	HandlerType: (*MailboxServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "Send",
+			Handler:    _Mailbox_Send_Handler,
+		},
+		{
+			MethodName: "List",
+			Handler:    _Mailbox_List_Handler,
+		},
+		{
+			MethodName: "MarkAsRead",
+			Handler:    _Mailbox_MarkAsRead_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "mailbox/mailbox.proto",
+}
diff --git a/proto/mocks/MailboxClient.go b/proto/mocks/MailboxClient.go
new file mode 100644
index 00000000..bc15d49e
--- /dev/null
+++ b/proto/mocks/MailboxClient.go
@@ -0,0 +1,144 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	grpc "google.golang.org/grpc"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+
+	mailbox "git.perx.ru/perxis/perxis-go/proto/mailbox"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MailboxClient is an autogenerated mock type for the MailboxClient type
+type MailboxClient struct {
+	mock.Mock
+}
+
+// List provides a mock function with given fields: ctx, in, opts
+func (_m *MailboxClient) List(ctx context.Context, in *mailbox.ListMessageRequest, opts ...grpc.CallOption) (*mailbox.ListMessageResponse, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, in)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	if len(ret) == 0 {
+		panic("no return value specified for List")
+	}
+
+	var r0 *mailbox.ListMessageResponse
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.ListMessageRequest, ...grpc.CallOption) (*mailbox.ListMessageResponse, error)); ok {
+		return rf(ctx, in, opts...)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.ListMessageRequest, ...grpc.CallOption) *mailbox.ListMessageResponse); ok {
+		r0 = rf(ctx, in, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*mailbox.ListMessageResponse)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *mailbox.ListMessageRequest, ...grpc.CallOption) error); ok {
+		r1 = rf(ctx, in, opts...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MarkAsRead provides a mock function with given fields: ctx, in, opts
+func (_m *MailboxClient) MarkAsRead(ctx context.Context, in *mailbox.MarkMessagesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, in)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	if len(ret) == 0 {
+		panic("no return value specified for MarkAsRead")
+	}
+
+	var r0 *emptypb.Empty
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.MarkMessagesRequest, ...grpc.CallOption) (*emptypb.Empty, error)); ok {
+		return rf(ctx, in, opts...)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.MarkMessagesRequest, ...grpc.CallOption) *emptypb.Empty); ok {
+		r0 = rf(ctx, in, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*emptypb.Empty)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *mailbox.MarkMessagesRequest, ...grpc.CallOption) error); ok {
+		r1 = rf(ctx, in, opts...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Send provides a mock function with given fields: ctx, in, opts
+func (_m *MailboxClient) Send(ctx context.Context, in *mailbox.SendMessageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, in)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Send")
+	}
+
+	var r0 *emptypb.Empty
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.SendMessageRequest, ...grpc.CallOption) (*emptypb.Empty, error)); ok {
+		return rf(ctx, in, opts...)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.SendMessageRequest, ...grpc.CallOption) *emptypb.Empty); ok {
+		r0 = rf(ctx, in, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*emptypb.Empty)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *mailbox.SendMessageRequest, ...grpc.CallOption) error); ok {
+		r1 = rf(ctx, in, opts...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// NewMailboxClient creates a new instance of MailboxClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMailboxClient(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MailboxClient {
+	mock := &MailboxClient{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/proto/mocks/MailboxServer.go b/proto/mocks/MailboxServer.go
new file mode 100644
index 00000000..8dcf0055
--- /dev/null
+++ b/proto/mocks/MailboxServer.go
@@ -0,0 +1,126 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	mailbox "git.perx.ru/perxis/perxis-go/proto/mailbox"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MailboxServer is an autogenerated mock type for the MailboxServer type
+type MailboxServer struct {
+	mock.Mock
+}
+
+// List provides a mock function with given fields: _a0, _a1
+func (_m *MailboxServer) List(_a0 context.Context, _a1 *mailbox.ListMessageRequest) (*mailbox.ListMessageResponse, error) {
+	ret := _m.Called(_a0, _a1)
+
+	if len(ret) == 0 {
+		panic("no return value specified for List")
+	}
+
+	var r0 *mailbox.ListMessageResponse
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.ListMessageRequest) (*mailbox.ListMessageResponse, error)); ok {
+		return rf(_a0, _a1)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.ListMessageRequest) *mailbox.ListMessageResponse); ok {
+		r0 = rf(_a0, _a1)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*mailbox.ListMessageResponse)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *mailbox.ListMessageRequest) error); ok {
+		r1 = rf(_a0, _a1)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MarkAsRead provides a mock function with given fields: _a0, _a1
+func (_m *MailboxServer) MarkAsRead(_a0 context.Context, _a1 *mailbox.MarkMessagesRequest) (*emptypb.Empty, error) {
+	ret := _m.Called(_a0, _a1)
+
+	if len(ret) == 0 {
+		panic("no return value specified for MarkAsRead")
+	}
+
+	var r0 *emptypb.Empty
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.MarkMessagesRequest) (*emptypb.Empty, error)); ok {
+		return rf(_a0, _a1)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.MarkMessagesRequest) *emptypb.Empty); ok {
+		r0 = rf(_a0, _a1)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*emptypb.Empty)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *mailbox.MarkMessagesRequest) error); ok {
+		r1 = rf(_a0, _a1)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Send provides a mock function with given fields: _a0, _a1
+func (_m *MailboxServer) Send(_a0 context.Context, _a1 *mailbox.SendMessageRequest) (*emptypb.Empty, error) {
+	ret := _m.Called(_a0, _a1)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Send")
+	}
+
+	var r0 *emptypb.Empty
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.SendMessageRequest) (*emptypb.Empty, error)); ok {
+		return rf(_a0, _a1)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *mailbox.SendMessageRequest) *emptypb.Empty); ok {
+		r0 = rf(_a0, _a1)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*emptypb.Empty)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *mailbox.SendMessageRequest) error); ok {
+		r1 = rf(_a0, _a1)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mustEmbedUnimplementedMailboxServer provides a mock function with no fields
+func (_m *MailboxServer) mustEmbedUnimplementedMailboxServer() {
+	_m.Called()
+}
+
+// NewMailboxServer creates a new instance of MailboxServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMailboxServer(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MailboxServer {
+	mock := &MailboxServer{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/proto/mocks/UnsafeMailboxServer.go b/proto/mocks/UnsafeMailboxServer.go
new file mode 100644
index 00000000..3b553203
--- /dev/null
+++ b/proto/mocks/UnsafeMailboxServer.go
@@ -0,0 +1,29 @@
+// Code generated by mockery v2.50.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// UnsafeMailboxServer is an autogenerated mock type for the UnsafeMailboxServer type
+type UnsafeMailboxServer struct {
+	mock.Mock
+}
+
+// mustEmbedUnimplementedMailboxServer provides a mock function with no fields
+func (_m *UnsafeMailboxServer) mustEmbedUnimplementedMailboxServer() {
+	_m.Called()
+}
+
+// NewUnsafeMailboxServer creates a new instance of UnsafeMailboxServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewUnsafeMailboxServer(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *UnsafeMailboxServer {
+	mock := &UnsafeMailboxServer{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
-- 
GitLab