Skip to content

fix(gmail): restore base64url padding before decoding message bodies - #200

Merged
peteski22 merged 5 commits into
mozilla-ai:mainfrom
shoemoney:fix/gmail-body-base64url-padding
Aug 7, 2026
Merged

fix(gmail): restore base64url padding before decoding message bodies#200
peteski22 merged 5 commits into
mozilla-ai:mainfrom
shoemoney:fix/gmail-body-base64url-padding

Conversation

@shoemoney

@shoemoney shoemoney commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Addresses the first half of #169.

The bug

The Gmail API returns MessagePartBody.data as base64url that commonly omits the trailing =, which base64.urlsafe_b64decode rejects with binascii.Error: Incorrect padding.

_extract_body decodes that field at three sites with no padding restoration, and every one of them sits inside a try/except or contextlib.suppress:

body_data = payload.get("body", {}).get("data", "")
if body_data:
    try:
        return base64.urlsafe_b64decode(body_data).decode("utf-8")
    except Exception:
        return "(Could not decode email body)"

So an unpadded body never raises. It silently becomes "(Could not decode email body)" on the single-part path, or falls through to "(No email body found)" on the multipart path — a perfectly readable email arriving at the caller as placeholder text. Padding is only present when the decoded length is a multiple of 3, so this fires on roughly two thirds of message lengths.

The fix

gmail_get_attachment already restores the padding, with a comment explaining why. This lifts that normalization into a shared _decode_base64url helper and routes the three _extract_body sites and gmail_get_attachment through it, so the module handles the wire format the same way everywhere — the shared helper #169 asks for.

The restoration is a no-op on already-padded input, so nothing that worked before changes.

Verification

Both new tests fail on main without the source change:

FAILED TestReadEmail::test_success_with_unpadded_single_part_body
    assert '(Could not decode email body)' == 'Hi Bob, following up on the thread.'
FAILED TestReadEmail::test_success_with_unpadded_multipart_body
    assert '(No email body found)' == 'Hi Bob, following up on the thread.'

They are modeled on the existing test_success_with_unpadded_data, which already covers this class of bug for the attachment path.

With the change:

$ uv run -m pytest tests -q
2317 passed, 49 skipped in 10.57s

$ uv run pre-commit run --all-files
check for merge conflicts................................................Passed
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
ruff (legacy alias)......................................................Passed
ruff format..............................................................Passed
Detect secrets...........................................................Passed
ty.......................................................................Passed
uv-lock..................................................................Passed

Deliberately not fixed here

The second half of #169 — the ~10 unguarded resp.json() sites across the module — is a separate concern and is left for its own PR. Happy to follow up with it if you'd like it in the same pass.

Summary by CodeRabbit

  • Bug Fixes

    • Improved Gmail email and attachment decoding when Base64URL data has missing padding.
    • Ensured single-part and multipart email bodies decode correctly, including HTML content.
    • Preserved clear fallback handling for undecodable or non-UTF-8 message content.
  • Tests

    • Added coverage for unpadded Base64URL content in Gmail messages and attachments.
    • Added tests for decoding failures and non-UTF-8 email bodies.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

Gmail tools now use shared strict Base64URL decoding for email bodies, MIME parts, and attachments. Tests cover unpadded data, invalid Base64URL, non-UTF-8 content, and attachment sizes.

Gmail Base64URL decoding

Layer / File(s) Summary
Shared decoder integration
src/apron_tools/providers/google/gmail/tools.py
Added _decode_base64url. Body, MIME-part, and attachment decoding now use the helper. Body paths handle ValueError explicitly.
Body and attachment decoding tests
tests/providers/google/gmail/test_tools.py
Added coverage for unpadded plain-text and HTML bodies, invalid and non-UTF-8 data, and attachment bytes and sizes.

Possibly related issues

  • mozilla-ai/apron-tools#169 — Covers the Gmail Base64URL padding issue addressed by this PR. It does not cover the separate resp.json() handling gap.

Possibly related PRs

  • mozilla-ai/apron-tools#168 — Updates the same Gmail decoding logic, including Base64URL padding and attachment decoding behaviour.

Suggested reviewers: peteski22

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: restoring Base64URL padding before decoding Gmail message bodies.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shoemoney
shoemoney force-pushed the fix/gmail-body-base64url-padding branch from cbe9dc5 to e06a08b Compare August 5, 2026 22:59
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/providers/google/gmail/test_tools.py`:
- Around line 198-220: Extend the regression tests around
test_success_with_unpadded_single_part_body and the gmail_get_attachment
coverage to exercise unpadded text/html decoding and attachment decoding. Use a
body whose Base64 representation ends with “==” so both padding characters are
removed, while preserving the existing successful result assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4063dd8a-c043-452d-a4f1-3498777e26ee

📥 Commits

Reviewing files that changed from the base of the PR and between a4fbb6d and e06a08b.

📒 Files selected for processing (2)
  • src/apron_tools/providers/google/gmail/tools.py
  • tests/providers/google/gmail/test_tools.py

Comment thread tests/providers/google/gmail/test_tools.py
shoemoney and others added 3 commits August 6, 2026 14:15
The Gmail API returns MessagePartBody.data as base64url that commonly
omits the trailing '=', which base64.urlsafe_b64decode rejects with
binascii.Error. _extract_body decoded that field directly at three sites
with no padding normalization, and every one of them sits inside a
try/except or contextlib.suppress -- so an unpadded body did not raise,
it silently became "(Could not decode email body)" or "(No email body
found)".

gmail_get_attachment already restores the padding. This lifts that
normalization into a shared _decode_base64url helper and routes the three
_extract_body sites plus gmail_get_attachment through it, so the module
handles the wire format consistently.

Addresses the first half of mozilla-ai#169.

Two tests, both verified failing without the change: the single-part path
returned "(Could not decode email body)" and the multipart text/plain
path returned "(No email body found)".
…e sites

Review follow-up to the shared _decode_base64url helper. The four decode
sites caught failure inconsistently: _extract_body used broad
`except Exception` / `contextlib.suppress(Exception)`, while
gmail_get_attachment caught `(binascii.Error, ValueError)`. Both
binascii.Error (bad base64) and UnicodeDecodeError (bad UTF-8) subclass
ValueError, so every site now catches ValueError -- narrower at the body
sites (no longer swallowing unrelated bugs) and without the redundant
tuple (binascii.Error subclasses ValueError), which lets the now-unused
binascii import go.

Document the helper's decode-failure contract, noting the decode is
lenient and does not validate the payload, and add regression tests
pinning both failure modes (invalid base64 length; valid base64 that
decodes to non-UTF-8 bytes) at the narrowed catch sites.
@peteski22
peteski22 force-pushed the fix/gmail-body-base64url-padding branch from 23109b4 to 9db3ff6 Compare August 6, 2026 14:32
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/apron_tools/providers/google/gmail/tools.py`:
- Around line 60-80: Update _decode_base64url to validate the padded Base64URL
input using base64.b64decode with altchars=b"-_" and validate=True, rather than
lenient urlsafe_b64decode; preserve padding restoration and allow valid padded
or unpadded input. Add regression coverage for an invalid alphabet character,
alongside the existing invalid-length case, and ensure both raise ValueError.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a709ce28-9a35-4cd8-b4f2-b9ca3462295d

📥 Commits

Reviewing files that changed from the base of the PR and between ce971b4 and 9db3ff6.

📒 Files selected for processing (2)
  • src/apron_tools/providers/google/gmail/tools.py
  • tests/providers/google/gmail/test_tools.py

Comment thread src/apron_tools/providers/google/gmail/tools.py Outdated

@peteski22 peteski22 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @shoemoney .. I added some fixups on top, but looks almost ready to go!

(Had to rebase just FYI)

Lenient urlsafe_b64decode silently discards characters outside the
base64url alphabet, so corrupt Gmail data decoded to truncated bytes
instead of raising. Decode with validate=True and altchars=b"-_" so
those payloads surface as a decode failure, and cover the invalid-
alphabet case alongside the existing invalid-length one.
@shoemoney

Copy link
Copy Markdown
Contributor Author

Pushed in 20ec75d — thanks for the rebase and the fixups.

_decode_base64url now decodes with base64.b64decode(..., altchars=b"-_", validate=True) instead of the lenient urlsafe_b64decode, so a character outside the base64url alphabet raises rather than being discarded into truncated bytes. Padding restoration is unchanged, and binascii.Error is still a ValueError subclass so every existing catch site is unaffected.

Added test_single_part_body_with_invalid_base64_alphabet_returns_placeholder to cover that path next to the existing invalid-length case. One note: + and / are not usable as the invalid character here — altchars translation leaves them in the standard alphabet, so they still validate. The test uses *, which is in neither alphabet.

66 passing locally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/apron_tools/providers/google/gmail/tools.py (1)

66-80: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Enforce the documented Base64URL alphabet.

altchars=b"-_" makes - and _ alternatives, but + and / remain accepted. _decode_base64url() documents that characters outside the base64url alphabet are rejected, so ++== and //== contradict the helper contract.

Reject + and / before calling base64.b64decode, or update the helper contract to accept standard Base64 characters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/apron_tools/providers/google/gmail/tools.py` around lines 66 - 80, Update
_decode_base64url() to explicitly reject standard Base64 characters '+' and '/'
before calling base64.b64decode, preserving the documented Base64URL-only
alphabet and existing validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/providers/google/gmail/test_tools.py`:
- Around line 294-316: Update the gmail body decoding test in
test_single_part_body_with_invalid_base64_alphabet_returns_placeholder to use an
invalid Base64 character that the older lenient decoder would still accept after
trimming, rather than an input like ab*d that fails for padding reasons. Keep
the same gmail_read_email assertion path and replace the payload body data with
a complete Base64 block plus an invalid suffix so the strict decoder in the test
fixture is exercised correctly.

---

Outside diff comments:
In `@src/apron_tools/providers/google/gmail/tools.py`:
- Around line 66-80: Update _decode_base64url() to explicitly reject standard
Base64 characters '+' and '/' before calling base64.b64decode, preserving the
documented Base64URL-only alphabet and existing validation behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: accfe652-f566-404c-8343-22d0a6378d01

📥 Commits

Reviewing files that changed from the base of the PR and between 9db3ff6 and 20ec75d.

📒 Files selected for processing (2)
  • src/apron_tools/providers/google/gmail/tools.py
  • tests/providers/google/gmail/test_tools.py

Comment thread tests/providers/google/gmail/test_tools.py
…test

The regression added in 20ec75d used `ab*d`, which the lenient decoder
also rejects -- it discards `*`, leaving `abd`, which then fails the
padding check -- so the test passed even without the validate=True
change. Use `YWJj*`: a complete valid block plus a stray non-alphabet
character that the lenient decoder silently accepts (decoding "abc") but
strict validation rejects, so the test now fails without the fix.
@peteski22
peteski22 merged commit 4bd0a85 into mozilla-ai:main Aug 7, 2026
8 checks passed
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.

2 participants