Skip to content

X-GM-EXT-1 Gmail Label Extension for Proton Bridge#521

Open
bolausson wants to merge 3 commits intoProtonMail:masterfrom
bolausson:master
Open

X-GM-EXT-1 Gmail Label Extension for Proton Bridge#521
bolausson wants to merge 3 commits intoProtonMail:masterfrom
bolausson:master

Conversation

@bolausson
Copy link
Copy Markdown

Just to be clear - This was entirly coded with a LLM!

X-GM-EXT-1 Gmail Label Extension for Proton Bridge

Paperless-NGX uses Gmail's X-GM-LABELS IMAP extension to apply and query labels on emails after processing. Proton Bridge didn't support this, so we implemented it across Gluon and Bridge.

Proton Bridge

  • STORE backendMarkMessagesWithGmailLabels maps label names to Proton label IDs (auto-creating if needed), then calls LabelMessages/UnlabelMessages API. Messages stay in INBOX — labels are applied without moving.
  • FETCH/SEARCH backendGetGmailLabels calls GetMessage API to get LabelIDs, maps them back to names via the shared label cache, filtering for LabelTypeLabel only. Used by both FETCH and SEARCH operations.

Gluon (IMAP server library)

  • STORE X-GM-LABELS — Parses STORE +X-GM-LABELS ("Paperless") / -X-GM-LABELS commands, routes them through a new SetGmailLabels connector interface to the Bridge backend. Added X-GM-EXT-1 capability advertisement.
  • FETCH X-GM-LABELS — Parses FETCH (X-GM-LABELS) as a fetch attribute, calls GetGmailLabels on the connector, returns labels in Gmail format: X-GM-LABELS ("Paperless" "Notifications").
  • SEARCH X-GM-LABELS — Parses X-GM-LABELS as a search key so clients can filter messages by label (e.g. NOT X-GM-LABELS "Paperless"). The search operation calls GetGmailLabels on the connector for each candidate message with case-insensitive matching.
  • Pre-auth capability fix — Moved X-GM-EXT-1 to the pre-auth capability list. Python's imaplib checks capabilities before LOGIN and never re-reads them, so clients like Paperless-NGX weren't detecting support.

Tests

telnet localhost 1143
a LOGIN <some@example.com> <password>
a LIST "" "Labels/%"
a SELECT "INBOX"
a UID SEARCH UNSEEN
a UID FETCH <UID> (BODY.PEEK[HEADER.FIELDS (SUBJECT)])
a UID FETCH <UID> (X-GM-LABELS)
a UID STORE <UID> +X-GM-LABELS ("__paperless_test_label__")
a UID FETCH <UID> (X-GM-LABELS)
a UID SEARCH X-GM-LABELS "__paperless_test_label__"
# a UID SEARCH NOT X-GM-LABELS "__paperless_test_label__"
a SELECT "Labels/__paperless_test_label__"
a UID SEARCH UNSEEN
a SELECT "INBOX"
a UID STORE <UID> -X-GM-LABELS ("__paperless_test_label__")
a UID FETCH <UID> (X-GM-LABELS)
a DELETE "Labels/__paperless_test_label__"
a LIST "" "Labels/%"
a LOGOUT
$ telnet localhost 1143
Trying ::1...
Connected to localhost.
Escape character is '^]'.
* OK [CAPABILITY AUTH=PLAIN ID IDLE IMAP4rev1 MOVE STARTTLS UIDPLUS UNSELECT X-GM-EXT-1] Proton Mail Bridge 03.22.00 - gluon session ID 2
a LOGIN <some@example.com> <password>
a OK [CAPABILITY AUTH=PLAIN ID IDLE IMAP4rev1 MOVE STARTTLS UIDPLUS UNSELECT X-GM-EXT-1] Logged in
a LIST "" "Labels/%"
* LIST (\Unmarked) "/" "Labels/Follow up"
* LIST (\Marked) "/" "Labels/Important"
a OK LIST
a SELECT "INBOX"
* FLAGS ($Forwarded Forwarded \Deleted \Flagged \Seen)
* 26487 EXISTS
* 0 RECENT
* OK [PERMANENTFLAGS ($Forwarded Forwarded \Deleted \Flagged \Seen)] Flags permitted
* OK [UIDNEXT 26545] Predicted next UID
* OK [UIDVALIDITY 96066389] UIDs valid
* OK [UNSEEN 26486] Unseen messages
a OK [READ-WRITE] SELECT
a UID SEARCH UNSEEN
* SEARCH 26543 26544
a OK command completed in 19322 microsec.
a UID FETCH 26543 (BODY.PEEK[HEADER.FIELDS (SUBJECT)])
* 26486 FETCH (BODY[HEADER.FIELDS (SUBJECT)] {59}
Subject: Proton Bridge with X-GM-EXT-1 support - Test 4

 UID 26543)
* 26487 FETCH (FLAGS (\Seen))
a OK [EXPUNGEISSUED] command completed in 1382 microsec.
a UID FETCH 26543 (X-GM-LABELS)
* 26486 FETCH (X-GM-LABELS ("Bjoern") UID 26543)
a OK [EXPUNGEISSUED] command completed in 311935 microsec.
a UID STORE 26543 +X-GM-LABELS ("__paperless_test_label__")
a OK command completed in 679973 microsec.
a UID FETCH 26543 (X-GM-LABELS)
* 26486 FETCH (X-GM-LABELS ("Bjoern" "__paperless_test_label__") UID 26543)
a OK [EXPUNGEISSUED] command completed in 321493 microsec.
a UID SEARCH X-GM-LABELS "__paperless_test_label__"
* SEARCH 26543
a OK [EXPUNGEISSUED] command completed in 18271 microsec.
a SELECT "Labels/__paperless_test_label__"
* FLAGS ($Forwarded Forwarded \Deleted \Flagged \Seen)
* 1 EXISTS
* 1 RECENT
* OK [PERMANENTFLAGS ($Forwarded Forwarded \Deleted \Flagged \Seen)] Flags permitted
* OK [UIDNEXT 2] Predicted next UID
* OK [UIDVALIDITY 96519480] UIDs valid
* OK [UNSEEN 1] Unseen messages
a OK [READ-WRITE] SELECT
a UID SEARCH UNSEEN
* SEARCH 1
a OK command completed in 264 microsec.
a SELECT "INBOX"
* FLAGS ($Forwarded Forwarded \Deleted \Flagged \Seen)
* 26486 EXISTS
* 0 RECENT
* OK [PERMANENTFLAGS ($Forwarded Forwarded \Deleted \Flagged \Seen)] Flags permitted
* OK [UIDNEXT 26545] Predicted next UID
* OK [UIDVALIDITY 96066389] UIDs valid
* OK [UNSEEN 26486] Unseen messages
a OK [READ-WRITE] SELECT
a UID STORE 26543 -X-GM-LABELS ("__paperless_test_label__")
a OK command completed in 400725 microsec.
a UID FETCH 26543 (X-GM-LABELS)
* 26486 FETCH (X-GM-LABELS ("Bjoern") UID 26543)
a OK command completed in 284999 microsec.
a DELETE "Labels/__paperless_test_label__"
a OK DELETE
a LIST "" "Labels/%"
* LIST (\Unmarked) "/" "Labels/Follow up"
* LIST (\Marked) "/" "Labels/Important"
a OK LIST
a LOGOUT
* BYE
a OK LOGOUT
Connection closed by foreign host.

Paperless-ngx logs:

paperless.log

[2026-02-22 04:21:44,001] [DEBUG] [paperless.tasks] Executing plugin ConsumerPreflightPlugin
[2026-02-22 04:21:44,014] [INFO] [paperless.tasks] ConsumerPreflightPlugin completed with no message
[2026-02-22 04:21:44,014] [DEBUG] [paperless.tasks] Skipping plugin CollatePlugin
[2026-02-22 04:21:44,017] [DEBUG] [paperless.tasks] Skipping plugin BarcodePlugin
[2026-02-22 04:21:44,017] [DEBUG] [paperless.tasks] Executing plugin WorkflowTriggerPlugin
[2026-02-22 04:21:44,101] [INFO] [paperless.matching] Document did not match Workflow: Import-Bjoern
[2026-02-22 04:21:44,101] [DEBUG] [paperless.matching] ("Document source MailFetch not in ['ApiUpload']",)
[2026-02-22 04:21:44,101] [INFO] [paperless.matching] Document matched WorkflowTrigger 3 from Workflow: Import-Bjoern
[2026-02-22 04:21:44,149] [INFO] [paperless.matching] Document did not match Workflow: Import-Other
[2026-02-22 04:21:44,150] [DEBUG] [paperless.matching] ('Document path /tmp/paperless/paperless-mail-h3aukvuh/Support_Case_Analysis_Report.pdf does not match */consume/Other/*',)
[2026-02-22 04:21:44,150] [INFO] [paperless.tasks] WorkflowTriggerPlugin completed with: Applying WorkflowAction 1 from Workflow: Import-Bjoern
[2026-02-22 04:21:44,150] [DEBUG] [paperless.tasks] Executing plugin ConsumeTaskPlugin
[2026-02-22 04:21:44,150] [INFO] [paperless.consumer] Consuming Support_Case_Analysis_Report.pdf
[2026-02-22 04:21:44,152] [DEBUG] [paperless.consumer] Detected mime type: application/pdf
[2026-02-22 04:21:44,160] [DEBUG] [paperless.consumer] Parser: RasterisedDocumentParser
[2026-02-22 04:21:44,165] [DEBUG] [paperless.consumer] Parsing Support_Case_Analysis_Report.pdf...
[2026-02-22 04:21:44,213] [INFO] [paperless.parsing.tesseract] pdftotext exited 0
[2026-02-22 04:21:44,587] [DEBUG] [paperless.parsing.tesseract] Calling OCRmyPDF with args: {'input_file': PosixPath('/tmp/paperless/paperless-ngxjzpdrcty/Support_Case_Analysis_Report.pdf'), 'output_file': PosixPath('/tmp/paperless/paperless-xw3sekya/archive.pdf'), 'use_threads': True, 'jobs': 4, 'language': 'deu', 'output_type': 'pdfa', 'progress_bar': False, 'color_conversion_strategy': 'RGB', 'skip_text': True, 'clean': True, 'deskew': True, 'rotate_pages': True, 'rotate_pages_threshold': 12.0, 'sidecar': PosixPath('/tmp/paperless/paperless-xw3sekya/sidecar.txt'), 'invalidate_digital_signatures': True}
[2026-02-22 04:21:44,935] [INFO] [ocrmypdf._pipelines.ocr] Start processing 2 pages concurrently
[2026-02-22 04:21:44,936] [INFO] [ocrmypdf._pipeline] skipping all processing on this page
[2026-02-22 04:21:44,936] [INFO] [ocrmypdf._pipeline] skipping all processing on this page
[2026-02-22 04:21:44,938] [INFO] [ocrmypdf._pipelines.ocr] Postprocessing...
[2026-02-22 04:21:45,223] [INFO] [ocrmypdf._pipeline] Image optimization ratio: 1.00 savings: 0.0%
[2026-02-22 04:21:45,223] [INFO] [ocrmypdf._pipeline] Total file size ratio: 0.05 savings: -2059.4%
[2026-02-22 04:21:45,225] [INFO] [ocrmypdf._pipelines._common] Output file is a PDF/A-2b (as expected)
[2026-02-22 04:21:45,230] [DEBUG] [paperless.parsing.tesseract] Incomplete sidecar file: discarding.
[2026-02-22 04:21:45,307] [INFO] [paperless.parsing.tesseract] pdftotext exited 0
[2026-02-22 04:21:45,308] [DEBUG] [paperless.consumer] Generating thumbnail for Support_Case_Analysis_Report.pdf...
[2026-02-22 04:21:45,312] [DEBUG] [paperless.parsing] Execute: convert -density 300 -scale 500x5000> -alpha remove -strip -auto-orient -define pdf:use-cropbox=true /tmp/paperless/paperless-xw3sekya/archive.pdf[0] /tmp/paperless/paperless-xw3sekya/convert.webp
[2026-02-22 04:21:46,736] [INFO] [paperless.parsing] convert exited 0
[2026-02-22 04:21:50,063] [DEBUG] [paperless.consumer] Saving record to database
[2026-02-22 04:21:50,063] [DEBUG] [paperless.consumer] Creation date from st_mtime: 2026-02-22 04:21:43.368249+01:00
[2026-02-22 04:21:50,065] [DEBUG] [paperless.templating] Parsing Workflow Jinja template: Support_Case_Analysis_Report
[2026-02-22 04:21:51,333] [INFO] [paperless.handlers] Assigning correspondent Sonstige to 2026-02-22T04:21:43.368249+01:00 Support_Case_Analysis_Report
[2026-02-22 04:21:51,346] [INFO] [paperless.handlers] Assigning document type Information to 2026-02-22T04:21:43.368249+01:00 Sonstige Support_Case_Analysis_Report
[2026-02-22 04:21:51,365] [INFO] [paperless.handlers] Tagging "2026-02-22T04:21:43.368249+01:00 Sonstige Support_Case_Analysis_Report" with "Depot, Versicherung"
[2026-02-22 04:21:51,496] [DEBUG] [paperless.index] Index updated for document 7160.
[2026-02-22 04:21:51,733] [DEBUG] [paperless.consumer] Deleting original file /tmp/paperless/paperless-mail-h3aukvuh/Support_Case_Analysis_Report.pdf
[2026-02-22 04:21:51,734] [DEBUG] [paperless.consumer] Deleting working copy /tmp/paperless/paperless-ngxjzpdrcty/Support_Case_Analysis_Report.pdf
[2026-02-22 04:21:51,740] [DEBUG] [paperless.parsing.tesseract] Deleting directory /tmp/paperless/paperless-xw3sekya
[2026-02-22 04:21:51,741] [INFO] [paperless.consumer] Document 2026-02-22 Sonstige Support_Case_Analysis_Report consumption finished
[2026-02-22 04:21:51,746] [INFO] [paperless.tasks] ConsumeTaskPlugin completed with: Success. New document id 7160 created

mail.log

[2026-02-22 04:21:40,536] [DEBUG] [paperless_mail] Skipping mail preprocessor MailMessageDecryptor
[2026-02-22 04:21:40,536] [DEBUG] [paperless_mail] Processing mail account Proton
[2026-02-22 04:21:40,601] [DEBUG] [paperless_mail] GMAIL Label Support: True
[2026-02-22 04:21:40,602] [DEBUG] [paperless_mail] AUTH=PLAIN Support: True
[2026-02-22 04:21:41,069] [DEBUG] [paperless_mail] Account Proton: Processing 1 rule(s)
[2026-02-22 04:21:41,072] [DEBUG] [paperless_mail] Rule Proton.Bjoern-PDF: Selecting folder INBOX
[2026-02-22 04:21:41,536] [DEBUG] [paperless_mail] Rule Proton.Bjoern-PDF: Searching folder with criteria ((NOT (X-GM-LABELS "Paperless") UNKEYWORD Paperless) SINCE 23-Jan-2026)
[2026-02-22 04:21:43,363] [DEBUG] [paperless_mail] Rule Proton.Bjoern-PDF: Processing mail Fwd: Proton Bridge with X-GM-EXT-1 support - Test 4 from <mail>@gmail.com with 1 attachment(s)
[2026-02-22 04:21:43,371] [INFO] [paperless_mail] Rule Proton.Bjoern-PDF: Consuming attachment Support_Case_Analysis_Report.pdf from mail Fwd: Proton Bridge with X-GM-EXT-1 support - Test 4 from <mail>@gmail.com
[2026-02-22 04:21:43,415] [DEBUG] [paperless_mail] Rule Proton.Bjoern-PDF: Processed 117 matching mail(s)

celery.log

[2026-02-22 04:21:43,416] [INFO] [celery.worker.strategy] Task documents.tasks.consume_file[ecaf3755-6e0c-4675-b481-4a7b9100368a] received
[2026-02-22 04:21:43,429] [INFO] [celery.app.trace] Task paperless_mail.tasks.process_mail_accounts[576a8f56-0d12-4d7f-beef-c4b5d6e593fd] succeeded in 2.9539029439911246s: 'Added 1 document(s).'
[2026-02-22 04:21:43,455] [DEBUG] [celery.pool] TaskPool: Apply <function fast_trace_task at 0xffff8fc1fec0> (args:('documents.tasks.consume_file', 'ecaf3755-6e0c-4675-b481-4a7b9100368a', {'lang': 'py', 'task': 'documents.tasks.consume_file', 'id': 'ecaf3755-6e0c-4675-b481-4a7b9100368a', 'shadow': None, 'eta': None, 'expires': None, 'group': 'ce2b51ce-2378-486a-b713-a485338694e0', 'group_index': 0, 'retries': 0, 'timelimit': [None, None], 'root_id': '576a8f56-0d12-4d7f-beef-c4b5d6e593fd', 'parent_id': '576a8f56-0d12-4d7f-beef-c4b5d6e593fd', 'argsrepr': "(ConsumableDocument(source=<DocumentSource.MailFetch: 3>, original_file=PosixPath('/tmp/paperless/paperless-mail-h3aukvuh/Support_Case_Analysis_Report.pdf'), original_path=None, mailrule_id=5, mime_type='application/pdf'), DocumentMetadataOverrides(filename='Support_Case_Analysis_Report.pdf', title='Support_Case_Analysis_Report', correspondent_id=None, document_type_id=None, tag_ids=[23, 1], storage_path_id=None, created=None, asn=None, owner_id=3, view_users=None, view_groups=None, change_users=None, change_groups=None, custom_fields=None))", 'kwargsrepr': '{}',... kwargs:{})
[2026-02-22 04:21:51,770] [INFO] [celery.app.trace] Task documents.tasks.consume_file[ecaf3755-6e0c-4675-b481-4a7b9100368a] succeeded in 7.831161381996935s: 'Success. New document id 7160 created'
[2026-02-22 04:21:51,772] [INFO] [celery.worker.strategy] Task paperless_mail.mail.apply_mail_action[9a3c9d52-dca5-4202-a7bd-df514e19c837] received
[2026-02-22 04:21:51,773] [DEBUG] [celery.pool] TaskPool: Apply <function fast_trace_task at 0xffff8fc1fec0> (args:('paperless_mail.mail.apply_mail_action', '9a3c9d52-dca5-4202-a7bd-df514e19c837', {'lang': 'py', 'task': 'paperless_mail.mail.apply_mail_action', 'id': '9a3c9d52-dca5-4202-a7bd-df514e19c837', 'shadow': None, 'eta': None, 'expires': None, 'group': None, 'group_index': None, 'retries': 0, 'timelimit': [None, None], 'root_id': '576a8f56-0d12-4d7f-beef-c4b5d6e593fd', 'parent_id': 'ecaf3755-6e0c-4675-b481-4a7b9100368a', 'argsrepr': "(['Success. New document id 7160 created'],)", 'kwargsrepr': "{'rule_id': 5, 'message_uid': '26546', 'message_subject': 'Fwd: Proton Bridge with X-GM-EXT-1 support - Test 4', 'message_date': datetime.datetime(2026, 2, 22, 4, 21, 9, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))}", 'origin': 'gen3817@edd8936e3312', 'ignore_result': False, 'replaced_task_nesting': 0, 'stamped_headers': None, 'stamps': {}, 'properties': {'correlation_id': '9a3c9d52-dca5-4202-a7bd-df514e19c837', 'reply_to': '31e8b107-1a2b-308d-af48-f397b6cd116d', 'delivery_mode': 2, 'delivery_info':... kwargs:{})
[2026-02-22 04:21:54,379] [INFO] [celery.app.trace] Task paperless_mail.mail.apply_mail_action[9a3c9d52-dca5-4202-a7bd-df514e19c837] succeeded in 1.243677235004725s: None
image

B. Olausson and others added 3 commits February 21, 2026 23:48
Implement the Bridge side of the X-GM-EXT-1 IMAP extension. When an
IMAP client sends STORE +X-GM-LABELS ("LabelName"), the Bridge resolves
the label name to a Proton label ID and calls LabelMessages/UnlabelMessages
without modifying folder membership (messages stay in INBOX).

Changes:
- Add GetLabelByName lookup to shared labels interface
- Implement MarkMessagesWithGmailLabels on Bridge connector
- Auto-create Proton labels when applying non-existent label names
- Point go.mod to local gluon with X-GM-EXT-1 support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add GetGmailLabels to the Bridge connector, enabling IMAP clients to
read which Proton labels (LabelTypeLabel) a message has via FETCH
X-GM-LABELS. Retrieves label IDs from the Proton API and maps them
to human-readable names through the shared labels cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Returns the IMAP mailbox ID for a Gmail label name by looking up the
in-memory label cache. Used by Gluon's optimized SEARCH X-GM-LABELS
to avoid per-message API calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant