+
@@ -229,6 +229,7 @@
autocomplete="off"
:placeholder="t('mail', 'Subject …')"
@input="saveDraftDebounced">
+
{{ t('mail', 'This message came from a noreply address so your reply will probably not be read.') }}
@@ -532,6 +533,7 @@ import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
import Download from 'vue-material-design-icons/TrayArrowDown.vue'
import IconUpload from 'vue-material-design-icons/TrayArrowUp.vue'
import ComposerAttachments from './ComposerAttachments.vue'
+import GovernanceLabelPicker from './GovernanceLabelPicker.vue'
import MailvelopeEditor from './MailvelopeEditor.vue'
import RecipientListItem from './RecipientListItem.vue'
import TextBlockModal from './textBlocks/TextBlockModal.vue'
@@ -567,6 +569,7 @@ export default {
ActionRadio,
ButtonVue,
ComposerAttachments,
+ GovernanceLabelPicker,
TextBlockModal,
ChevronLeft,
Delete,
@@ -717,6 +720,11 @@ export default {
default: false,
},
+ governanceLabelId: {
+ type: String,
+ default: null,
+ },
+
accounts: {
type: Array,
required: true,
@@ -755,6 +763,7 @@ export default {
editorMode: (this.body?.format !== 'html') ? EDITOR_MODE_TEXT : EDITOR_MODE_HTML,
requestMdnVal: this.requestMdn,
+ governanceLabelIdVal: this.governanceLabelId,
changeSignature: false,
loadingIndicatorTo: false,
loadingIndicatorCc: false,
@@ -1070,6 +1079,10 @@ export default {
this.$emit('update:request-mdn', val)
},
+ governanceLabelIdVal(val) {
+ this.$emit('update:governance-label-id', val)
+ },
+
selectedAlias: {
handler() {
const aliasEmailAddress = this.selectedAlias.emailAddress
@@ -1264,6 +1277,7 @@ export default {
inReplyToMessageId: this.inReplyToMessageId ?? (this.replyTo ? this.replyTo.messageId : undefined),
isHtml: !this.encrypt && !this.editorPlainText,
requestMdn: this.requestMdnVal,
+ governanceLabelId: this.governanceLabelIdVal ?? undefined,
sendAt: this.sendAtVal !== 0 ? Math.floor(this.sendAtVal / 1000) : undefined,
smimeSign: this.shouldSmimeSign,
smimeEncrypt: this.shouldSmimeEncrypt,
@@ -1827,6 +1841,12 @@ export default {
}
}
+ &--subject {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
.subject {
font-size: 15px;
font-weight: bold;
diff --git a/src/components/GovernanceLabelChip.vue b/src/components/GovernanceLabelChip.vue
new file mode 100644
index 0000000000..d970d77839
--- /dev/null
+++ b/src/components/GovernanceLabelChip.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+ {{ label.name }}
+
+
+
+
+
+
diff --git a/src/components/GovernanceLabelPicker.vue b/src/components/GovernanceLabelPicker.vue
new file mode 100644
index 0000000000..0314ba01a3
--- /dev/null
+++ b/src/components/GovernanceLabelPicker.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+ {{ option.name }}
+
+
+
+
+
+
+
+ {{ option.name }}
+
+
+
+
+
+
+
+
diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue
index 70bf666100..ca35d0f472 100644
--- a/src/components/NewMessageModal.vue
+++ b/src/components/NewMessageModal.vue
@@ -92,6 +92,7 @@
:is-first-open="modalFirstOpen"
:is-draft="composerData.draftId !== undefined"
:request-mdn="composerData.requestMdn"
+ :governance-label-id="composerData.governanceLabelId"
:accounts="accounts"
@update:from-account="patchComposerData({ accountId: $event })"
@update:from-alias="patchComposerData({ aliasId: $event })"
@@ -105,6 +106,7 @@
@update:smime-sign="patchComposerData({ smimeSign: $event })"
@update:smime-encrypt="patchComposerData({ smimeSign: $event })"
@update:request-mdn="patchComposerData({ requestMdn: $event })"
+ @update:governance-label-id="patchComposerData({ governanceLabelId: $event })"
@draft="onDraft"
@discard-draft="discardDraft"
@upload-attachment="onAttachmentUploading"
diff --git a/src/components/ThreadEnvelope.vue b/src/components/ThreadEnvelope.vue
index 92e3415034..7328a1b48c 100644
--- a/src/components/ThreadEnvelope.vue
+++ b/src/components/ThreadEnvelope.vue
@@ -96,6 +96,9 @@
+
}
+ */
+export async function fetchGovernanceLabels() {
+ const url = generateUrl('/apps/mail/api/governance/labels')
+
+ const response = await axios.get(url)
+ return response.data.data
+}
+
+let labelsPromise = null
+
+/**
+ * Fetch governance labels once and cache them for the session.
+ * Resolves to an empty list when the governance app is not available.
+ *
+ * @return {Promise<{id: string, type: string, name: string, priority: number, description: string, color: string, scopes: string[]}[]>}
+ */
+export function getGovernanceLabels() {
+ if (!loadState('mail', 'governance-labels-available', false)) {
+ return Promise.resolve([])
+ }
+
+ if (labelsPromise === null) {
+ labelsPromise = fetchGovernanceLabels()
+ }
+ return labelsPromise
+}
diff --git a/tests/Integration/Service/MailTransmissionIntegrationTest.php b/tests/Integration/Service/MailTransmissionIntegrationTest.php
index ca124f63d4..8ccbfee693 100644
--- a/tests/Integration/Service/MailTransmissionIntegrationTest.php
+++ b/tests/Integration/Service/MailTransmissionIntegrationTest.php
@@ -35,6 +35,7 @@
use OCA\Mail\Send\SentMailboxHandler;
use OCA\Mail\Service\AliasesService;
use OCA\Mail\Service\Attachment\UploadedFile;
+use OCA\Mail\Service\GovernanceLabelService;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\TransmissionService;
use OCA\Mail\SMTP\SmtpClientFactory;
@@ -141,7 +142,8 @@ protected function setUp(): void {
Server::get(PerformanceLogger::class),
Server::get(AliasesService::class),
Server::get(TransmissionService::class),
- Server::get(IMailManager::class)
+ Server::get(IMailManager::class),
+ Server::get(GovernanceLabelService::class)
);
}
diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php
index 9b84226810..42c7b27c72 100644
--- a/tests/Unit/Controller/PageControllerTest.php
+++ b/tests/Unit/Controller/PageControllerTest.php
@@ -21,6 +21,7 @@
use OCA\Mail\Service\AliasesService;
use OCA\Mail\Service\Classification\ClassificationSettingsService;
use OCA\Mail\Service\ContextChat\ContextChatSettingsService;
+use OCA\Mail\Service\GovernanceLabelService;
use OCA\Mail\Service\InternalAddressService;
use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\OutboxService;
@@ -116,6 +117,8 @@ class PageControllerTest extends TestCase {
private ContextChatSettingsService $contextChatSettingsService;
private ClassificationSettingsService|MockObject $classificationSettingsService;
+
+ private GovernanceLabelService|MockObject $governanceLabelService;
protected function setUp(): void {
parent::setUp();
@@ -147,6 +150,7 @@ protected function setUp(): void {
$this->contextChatSettingsService->method('isIndexingEnabled')->willReturn(true);
$this->classificationSettingsService = $this->createMock(ClassificationSettingsService::class);
+ $this->governanceLabelService = $this->createMock(GovernanceLabelService::class);
$this->controller = new PageController(
$this->appName,
$this->request,
@@ -172,7 +176,8 @@ protected function setUp(): void {
$this->quickActionsService,
$this->appManager,
$this->contextChatSettingsService,
- $this->classificationSettingsService
+ $this->classificationSettingsService,
+ $this->governanceLabelService
);
}
@@ -332,12 +337,16 @@ public function testIndex(): void {
$this->classificationSettingsService->expects(($this->once()))
->method(('isClassificationEnabledByDefault'))
->willReturn(true);
- $this->initialState->expects($this->exactly(27))
+ $this->governanceLabelService->expects($this->once())
+ ->method('isGovernanceAvailable')
+ ->willReturn(false);
+ $this->initialState->expects($this->exactly(28))
->method('provideInitialState')
->withConsecutive(
['debug', true],
['ncVersion', '26.0.0'],
['mailVersion', '0.0.1-dev.0'],
+ ['governance-labels-available', false],
['accounts', $accountsJson],
['account-settings', []],
['tags', []],
diff --git a/tests/Unit/IMAP/PreviewEnhancerTest.php b/tests/Unit/IMAP/PreviewEnhancerTest.php
index 6a08b3deb7..fe805ae838 100644
--- a/tests/Unit/IMAP/PreviewEnhancerTest.php
+++ b/tests/Unit/IMAP/PreviewEnhancerTest.php
@@ -21,6 +21,7 @@
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Avatar\Avatar;
use OCA\Mail\Service\AvatarService;
+use OCA\Mail\Service\GovernanceLabelService;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@@ -41,6 +42,8 @@ class PreviewEnhancerTest extends TestCase {
/** @var AttachmentService|MockObject */
private $attachmentService;
+ private $governanceLabelService;
+
protected function setUp(): void {
parent::setUp();
@@ -50,6 +53,7 @@ protected function setUp(): void {
$this->logger = $this->createMock(LoggerInterface::class);
$this->avatarService = $this->createMock(AvatarService::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
+ $this->governanceLabelService = $this->createMock(GovernanceLabelService::class);
$this->previewEnhancer = new PreviewEnhancer(
$this->imapClientFactory,
@@ -57,7 +61,8 @@ protected function setUp(): void {
$this->dbMapper,
$this->logger,
$this->avatarService,
- $this->attachmentService
+ $this->attachmentService,
+ $this->governanceLabelService
);
}
diff --git a/tests/Unit/Service/GovernanceLabelServiceTest.php b/tests/Unit/Service/GovernanceLabelServiceTest.php
new file mode 100644
index 0000000000..eaf3cff46a
--- /dev/null
+++ b/tests/Unit/Service/GovernanceLabelServiceTest.php
@@ -0,0 +1,121 @@
+appManager = $this->createMock(IAppManager::class);
+ $this->container = $this->createMock(ContainerInterface::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->service = new GovernanceLabelService(
+ $this->appManager,
+ $this->container,
+ $this->config,
+ );
+ }
+
+ public function testGetLabelsWhenGovernanceIsNotEnabled(): void {
+ $this->appManager->method('isEnabledForAnyone')
+ ->with('governance')
+ ->willReturn(false);
+ $this->container->expects(self::never())
+ ->method('get');
+
+ $labels = $this->service->getLabels();
+
+ self::assertSame([], $labels);
+ }
+
+ public function testGetLabelWhenGovernanceIsNotEnabled(): void {
+ $this->appManager->method('isEnabledForAnyone')
+ ->with('governance')
+ ->willReturn(false);
+
+ $label = $this->service->getLabel('123');
+
+ self::assertNull($label);
+ }
+
+ public function testBuildHeaderValue(): void {
+ $this->config->method('getSystemValueString')
+ ->with('instanceid')
+ ->willReturn('abc123');
+
+ $value = $this->service->buildHeaderValue('42');
+
+ self::assertSame('id=42; origin=abc123', $value);
+ }
+
+ public function testResolveHeaderLabelId(): void {
+ $this->config->method('getSystemValueString')
+ ->with('instanceid')
+ ->willReturn('abc123');
+ $service = $this->getMockBuilder(GovernanceLabelService::class)
+ ->setConstructorArgs([$this->appManager, $this->container, $this->config])
+ ->onlyMethods(['getLabel'])
+ ->getMock();
+ $service->method('getLabel')
+ ->with('42')
+ ->willReturn(['id' => '42']);
+
+ $labelId = $service->resolveHeaderLabelId('id=42; origin=abc123');
+
+ self::assertSame('42', $labelId);
+ }
+
+ public function testResolveHeaderLabelIdWithForeignOrigin(): void {
+ $this->config->method('getSystemValueString')
+ ->with('instanceid')
+ ->willReturn('abc123');
+
+ $labelId = $this->service->resolveHeaderLabelId('id=42; origin=otherinstance');
+
+ self::assertNull($labelId);
+ }
+
+ public function testResolveHeaderLabelIdWithUnknownLabel(): void {
+ $this->config->method('getSystemValueString')
+ ->with('instanceid')
+ ->willReturn('abc123');
+ $this->appManager->method('isEnabledForAnyone')
+ ->with('governance')
+ ->willReturn(false);
+
+ $labelId = $this->service->resolveHeaderLabelId('id=42; origin=abc123');
+
+ self::assertNull($labelId);
+ }
+
+ public function testResolveHeaderLabelIdWithGarbage(): void {
+ $labelId = $this->service->resolveHeaderLabelId('not a valid header');
+
+ self::assertNull($labelId);
+ }
+
+ public function testResolveHeaderLabelIdWithNull(): void {
+ $labelId = $this->service->resolveHeaderLabelId(null);
+
+ self::assertNull($labelId);
+ }
+}
diff --git a/tests/Unit/Service/MailTransmissionTest.php b/tests/Unit/Service/MailTransmissionTest.php
index fe2925fbf8..ebecd3f997 100644
--- a/tests/Unit/Service/MailTransmissionTest.php
+++ b/tests/Unit/Service/MailTransmissionTest.php
@@ -30,6 +30,7 @@
use OCA\Mail\Model\Message;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Service\AliasesService;
+use OCA\Mail\Service\GovernanceLabelService;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\TransmissionService;
use OCA\Mail\SMTP\SmtpClientFactory;
@@ -51,6 +52,7 @@ class MailTransmissionTest extends TestCase {
private MailTransmission $transmission;
private AliasesService|MockObject $aliasService;
private TransmissionService $transmissionService;
+ private GovernanceLabelService|MockObject $governanceLabelService;
protected function setUp(): void {
parent::setUp();
@@ -65,6 +67,7 @@ protected function setUp(): void {
$this->aliasService = $this->createMock(AliasesService::class);
$this->transmissionService = $this->createMock(TransmissionService::class);
$this->mailManager = $this->createMock(IMailManager::class);
+ $this->governanceLabelService = $this->createMock(GovernanceLabelService::class);
$this->transmission = new MailTransmission(
$this->imapClientFactory,
@@ -77,6 +80,7 @@ protected function setUp(): void {
$this->aliasService,
$this->transmissionService,
$this->mailManager,
+ $this->governanceLabelService,
);
}