@@ -185,7 +185,8 @@ public function appendMessage(string $folder, string $rawMessage, array $flags,
185185 $ result = $ this ->imap ->append ($ folder , $ message , $ flags , $ internalDate , false );
186186 if (!$ result && $ this ->imap ->error ) {
187187 $ errorMessage = $ this ->getErrorMessage ("Could not append message to {$ folder }. " );
188- if (stripos ($ errorMessage , '[OVERQUOTA] ' ) !== false ) {
188+ $ resultCode = is_string ($ this ->imap ->resultcode ?? null ) ? $ this ->imap ->resultcode : null ;
189+ if (self ::isQuotaErrorResponse ($ resultCode , $ this ->imap ->error )) {
189190 throw new RoundcubeImapSyncQuotaExceededException ($ errorMessage );
190191 }
191192
@@ -195,6 +196,47 @@ public function appendMessage(string $folder, string $rawMessage, array $flags,
195196 return $ result ;
196197 }
197198
199+ /**
200+ * Decide whether an IMAP NO/BAD response signals "destination mailbox is over quota".
201+ *
202+ * Three signals, any one of which is sufficient:
203+ *
204+ * 1. RFC 5530 / RFC 9208 IMAP response code OVERQUOTA — what stock Dovecot
205+ * >= 2.2.30 returns. rcube_imap_generic extracts response codes into
206+ * `resultcode` and removes them from the human error text, which is
207+ * why we cannot just substring-match the error.
208+ * 2. RFC 3463 enhanced status code "5.2.2" ("Mailbox full") in the error
209+ * text. Operators with a custom `quota_exceeded_message` (e.g. the
210+ * Mittwald default) replace the standard response code with a sentence
211+ * keyed on 5.2.2 — we want to catch those too. Bounded with whitespace
212+ * so we don't false-positive on IP addresses or unrelated dotted-number
213+ * fragments.
214+ * 3. Substring match on "OVERQUOTA" in the error text — defensive catch
215+ * for setups where the tag survives into the human message but somehow
216+ * not as a response code.
217+ */
218+ public static function isQuotaErrorResponse (?string $ resultCode , ?string $ errorText ): bool
219+ {
220+ if ($ resultCode !== null && strcasecmp ($ resultCode , 'OVERQUOTA ' ) === 0 ) {
221+ return true ;
222+ }
223+
224+ $ error = (string ) $ errorText ;
225+ if ($ error === '' ) {
226+ return false ;
227+ }
228+
229+ if (preg_match ('/(?:^|\s)5\.2\.2(?:\s|$)/ ' , $ error ) === 1 ) {
230+ return true ;
231+ }
232+
233+ if (stripos ($ error , 'OVERQUOTA ' ) !== false ) {
234+ return true ;
235+ }
236+
237+ return false ;
238+ }
239+
198240 private function normalizeMessageId (?string $ messageId ): ?string
199241 {
200242 $ messageId = trim ((string ) $ messageId );
0 commit comments