diff --git a/appinfo/info.xml b/appinfo/info.xml
index 0fdb533cd0..bcc6603b6b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -34,7 +34,7 @@ The rating depends on the installed text processing backend. See [the rating ove
Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
]]>
- 5.10.0-rc.1
+ 5.11.0-dev.1
agpl
Christoph Wurst
GretaD
diff --git a/lib/Controller/DraftsController.php b/lib/Controller/DraftsController.php
index 89385b37e0..f757e0a096 100644
--- a/lib/Controller/DraftsController.php
+++ b/lib/Controller/DraftsController.php
@@ -87,7 +87,8 @@ public function create(
?int $sendAt = null,
?int $draftId = null,
bool $requestMdn = false,
- bool $isPgpMime = false) : JsonResponse {
+ bool $isPgpMime = false,
+ bool $isAiGenerated = false) : JsonResponse {
$effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId);
$account = $this->accountService->find($effectiveUserId, $accountId);
if ($draftId !== null) {
@@ -109,6 +110,7 @@ public function create(
$message->setSmimeEncrypt($smimeEncrypt);
$message->setRequestMdn($requestMdn);
$message->setPgpMime($isPgpMime);
+ $message->setAiGenerated($isAiGenerated);
if (!empty($smimeCertificateId)) {
$smimeCertificate = $this->smimeService->findCertificate($smimeCertificateId, $effectiveUserId);
@@ -161,7 +163,8 @@ public function update(int $id,
?int $smimeCertificateId = null,
?int $sendAt = null,
bool $requestMdn = false,
- bool $isPgpMime = false): JsonResponse {
+ bool $isPgpMime = false,
+ bool $isAiGenerated = false): JsonResponse {
$effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId);
$message = $this->service->getMessage($id, $effectiveUserId);
$account = $this->accountService->find($effectiveUserId, $accountId);
@@ -182,6 +185,7 @@ public function update(int $id,
$message->setSmimeEncrypt($smimeEncrypt);
$message->setRequestMdn($requestMdn);
$message->setPgpMime($isPgpMime);
+ $message->setAiGenerated($isAiGenerated);
if (!empty($smimeCertificateId)) {
$smimeCertificate = $this->smimeService->findCertificate($smimeCertificateId, $effectiveUserId);
diff --git a/lib/Controller/OutboxController.php b/lib/Controller/OutboxController.php
index c5a98780b2..002117edc4 100644
--- a/lib/Controller/OutboxController.php
+++ b/lib/Controller/OutboxController.php
@@ -103,6 +103,7 @@ public function create(
?int $sendAt = null,
bool $requestMdn = false,
bool $isPgpMime = false,
+ bool $isAiGenerated = false,
): JsonResponse {
$effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId);
$account = $this->accountService->find($effectiveUserId, $accountId);
@@ -126,6 +127,7 @@ public function create(
$message->setSmimeSign($smimeSign);
$message->setSmimeEncrypt($smimeEncrypt);
$message->setRequestMdn($requestMdn);
+ $message->setAiGenerated($isAiGenerated);
if (!empty($smimeCertificateId)) {
$smimeCertificate = $this->smimeService->findCertificate($smimeCertificateId, $effectiveUserId);
@@ -198,6 +200,7 @@ public function update(
?int $sendAt = null,
bool $requestMdn = false,
bool $isPgpMime = false,
+ bool $isAiGenerated = false,
): JsonResponse {
$effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId);
$message = $this->service->getMessage($id, $effectiveUserId);
@@ -219,6 +222,7 @@ public function update(
$message->setSmimeSign($smimeSign);
$message->setSmimeEncrypt($smimeEncrypt);
$message->setRequestMdn($requestMdn);
+ $message->setAiGenerated($isAiGenerated);
// Reset the status to make it retryable.
$message->setFailed(false);
diff --git a/lib/Db/LocalMessage.php b/lib/Db/LocalMessage.php
index 8e905ccef5..4b45134525 100644
--- a/lib/Db/LocalMessage.php
+++ b/lib/Db/LocalMessage.php
@@ -53,6 +53,8 @@
* @method setRaw(string|null $raw)
* @method bool getRequestMdn()
* @method setRequestMdn(bool $mdn)
+ * @method bool isAiGenerated()
+ * @method void setAiGenerated(bool $aiGenerated)
*/
class LocalMessage extends Entity implements JsonSerializable {
public const TYPE_OUTGOING = 0;
@@ -141,6 +143,9 @@ class LocalMessage extends Entity implements JsonSerializable {
/** @var bool */
protected $requestMdn;
+ /** @var bool */
+ protected $aiGenerated;
+
public function __construct() {
$this->addType('type', 'integer');
$this->addType('accountId', 'integer');
@@ -155,7 +160,7 @@ public function __construct() {
$this->addType('smimeEncrypt', 'boolean');
$this->addType('status', 'integer');
$this->addType('requestMdn', 'boolean');
-
+ $this->addType('aiGenerated', 'boolean');
}
#[\Override]
@@ -195,6 +200,7 @@ public function jsonSerialize() {
'status' => $this->getStatus(),
'raw' => $this->getRaw(),
'requestMdn' => $this->getRequestMdn(),
+ 'isAiGenerated' => $this->isAiGenerated() === true,
];
}
diff --git a/lib/IMAP/ImapMessageFetcher.php b/lib/IMAP/ImapMessageFetcher.php
index 71ff349e50..561c681d2e 100644
--- a/lib/IMAP/ImapMessageFetcher.php
+++ b/lib/IMAP/ImapMessageFetcher.php
@@ -55,6 +55,7 @@ class ImapMessageFetcher {
private string $rawReferences = '';
private string $dispositionNotificationTo = '';
private bool $hasDkimSignature = false;
+ private bool $isAiGenerated = false;
private array $phishingDetails = [];
private ?string $unsubscribeUrl = null;
private bool $isOneClickUnsubscribe = false;
@@ -262,6 +263,7 @@ public function fetchMessage(?Horde_Imap_Client_Data_Fetch $fetch = null): IMAPM
$this->rawReferences,
$this->dispositionNotificationTo,
$this->hasDkimSignature,
+ $this->isAiGenerated,
$this->phishingDetails,
$this->unsubscribeUrl,
$this->isOneClickUnsubscribe,
@@ -527,10 +529,8 @@ private function decodeSubject(Horde_Imap_Client_Data_Envelope $envelope): strin
}
private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void {
- /** @var resource $headersStream */
- $headersStream = $fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
- $parsedHeaders = Horde_Mime_Headers::parseHeaders($headersStream);
- fclose($headersStream);
+ /** @var Horde_Mime_Headers $parsedHeaders */
+ $parsedHeaders = $fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
$references = $parsedHeaders->getHeader('references');
if ($references !== null) {
@@ -545,6 +545,9 @@ private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void {
$dkimSignatureHeader = $parsedHeaders->getHeader('dkim-signature');
$this->hasDkimSignature = $dkimSignatureHeader !== null;
+ $aiGeneratedHeader = $parsedHeaders->getHeader('x-ai-generated');
+ $this->isAiGenerated = $aiGeneratedHeader !== null && trim($aiGeneratedHeader->value_single) === '1';
+
if ($this->runPhishingCheck) {
$this->phishingDetails = $this->phishingDetectionService->checkHeadersForPhishing($this->userId, $parsedHeaders, $fetch->getFlags(), $this->hasHtmlMessage, $this->htmlMessage);
}
diff --git a/lib/Migration/Version5201Date20260604000000.php b/lib/Migration/Version5201Date20260604000000.php
new file mode 100644
index 0000000000..7da87cfb7c
--- /dev/null
+++ b/lib/Migration/Version5201Date20260604000000.php
@@ -0,0 +1,36 @@
+getTable('mail_local_messages');
+ if (!$table->hasColumn('ai_generated')) {
+ $table->addColumn('ai_generated', Types::BOOLEAN, [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+ }
+
+ return $schema;
+ }
+}
diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php
index ed9d95a144..02495cbb08 100644
--- a/lib/Model/IMAPMessage.php
+++ b/lib/Model/IMAPMessage.php
@@ -72,6 +72,7 @@ public function __construct(
private string $rawReferences,
private string $dispositionNotificationTo,
private bool $hasDkimSignature,
+ private bool $isAiGenerated,
private array $phishingDetails,
private ?string $unsubscribeUrl,
private bool $isOneClickUnsubscribe,
@@ -293,6 +294,7 @@ public function jsonSerialize() {
'unsubscribeMailto' => $this->unsubscribeMailto,
'scheduling' => $this->scheduling,
'isPgpMimeEncrypted' => $this->isPgpMimeEncrypted,
+ 'aiGenerated' => $this->isAiGenerated,
];
}
diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php
index e58a36d930..d44f7b6f4b 100644
--- a/lib/Service/MailTransmission.php
+++ b/lib/Service/MailTransmission.php
@@ -138,6 +138,10 @@ public function sendMessage(Account $account, LocalMessage $localMessage): void
$fccHeaders->addHeaderOb(new Horde_Mime_Headers_Addresses(Horde_Mime_Mdn::MDN_HEADER, $from->toHorde()));
}
+ if ($localMessage->isAiGenerated()) {
+ $fccHeaders->addHeader('X-AI-Generated', '1');
+ }
+
// For SMTP delivery: strip Bcc so it never appears in the transmitted
// message (RFC 5321). All three recipient lists are passed as SMTP
// envelope recipients so every addressee still receives the mail.
diff --git a/src/components/Composer.vue b/src/components/Composer.vue
index 846c9ece14..e1d05434a7 100644
--- a/src/components/Composer.vue
+++ b/src/components/Composer.vue
@@ -400,6 +400,12 @@
@uncheck="requestMdnVal = false">
{{ t('mail', 'Request a read receipt') }}
+
+ {{ t('mail', 'Mark as AI generated') }}
+